The extra token is a SentencePiece underline token, which is a space token to indicate that the proceeding token is either the start of a word or a standalone token. You can check what the actual tokens are like this:
ids = tokenizer.encode("0")
tokens = tokenizer.convert_ids_to_tokens(ids)
print(tokens)
Which shows:
['▁', '0', '</s>']
The '▁' is the 3 token you’re seeing. When you look at the tokens themselves or the token ids, you can see what’s going on, though when you decode or print, they’re omitted.
In fact, when you use the T5 tokenizer, all words start with a space like this, just most of them have the space built into the token itself. For example, the token for “1” is actually this: '▁1' (it’s just one token, but contains a space and the character for the 1).
Meanwhile, for words that contain multiple tokens, the first one in the word will have the space while the others won’t. For example:
ids = tokenizer.encode("Onomatopoeia")
tokens = tokenizer.convert_ids_to_tokens(ids)
print(tokens)
You get:
['▁On', 'omato', 'p', 'o', 'e', 'i', 'a', '</s>']
Why the tokenizer adds a separate space token in front of “0”, but has it merged into the token for 1, is probably just a statistical quirk of how the SentencePiece algorithm worked when it was run on the corpus the Google people used to create the T5Tokenizer. There’s no real a priori reason.