In this loop, while start < len(s):, the string s is not changing, so the length of the string is not going to change either. Yet, for each iteration, you will evaluate len(s) in order to evaluate the loop condition. len(s) is undoubtably a fast operation, but you should still cache the result to avoid the function call every iteration.
limit = len(s)
while start < limit:
# ...
The loop for i in range(start, len(s)):, you are again calling len(s) for each iteration (of the outer loop). You could replace this with for i in range(start, limit):, but we will still have a different issue...
You are (mostly) not using i inside the loop; you are using s[i]. And you look up the character s[i] in 3 different places inside the loop. Instead of looping over the indices, and looking up the character at the index, you should directly loop over the characters of the string.
for ch in s[start:]:
# ...
Except for that pesky seen[...] = i statement. You still want the index. The enumerate() function can be used to count as you are looping:
for i, ch in enumerate(s[start:], start):
if ch in seen:
start = seen[ch]+1
seen = {}
break
else:
seen[ch] = i
size += 1
Hint
When scanning the string abcdefgdhijklmn, after encountering the second d, you reset to the character after the first d, and continue your scan. But ...
Do you have to? Aren’t all the characters between the first d and the second d unique? Is there any way you could preserve that information, and continue on, without needing to restart with the e? Maybe you don’t need nested loops!