I will go with "a bit like docstrings, then" and declare this harmless, as long as it is always None, or a narrow range of other values, all immutable.
It reeks of atavism, and excessive attachment to statically typed languages. And it does no good as code. But it has a minor purpose that remains, in documentation.
It documents what the expected names are, so if I combine code with someone and one of us has 'username' and the other user_name, there is a clue to the humans that we have parted ways and are not using the same variables.
Forcing full initialization as a policy achieves the same thing in a more Pythonic way, but if there is actual code in the __init__, this provides a clearer place to document the variables in use.
Obviously the BIG problem here is that it tempts folks to initialize with values other than None, which can be bad:
class X: v = {} x = X() x.v[1] = 2
leaves a global trace and does not create an instance for x.
But that is more of a quirk in Python as a whole than in this practice, and we should already be paranoid about it.
__init__already provides autocompletion etc. Also, usingNoneprevents the IDE to infer a better type for the attribute, so it's better to use a sensible default instead (when possible).typingmodule, which allow you to provide hints to the IDE and linter, if that sort of thing tickles your fancy...self. Even ifself.nameorself.agewere not assigned in__init__they would not show up in the instanceself, they only show up in the classPerson.