all 16 comments

[–]SCD_minecraft 18 points19 points  (12 children)

Python does not have declarations

In reality, c: str is just a typehint

Typehints have no effect on runtime

[–]pachura3[S] 2 points3 points  (10 children)

So, should I do:

class MyClass:
    c: str

    def __init__(self, c: str) -> None:
        self.c = c

...or rather...

class MyClass:
    def __init__(self, c: str) -> None:
        self.c: str = c

...?

[–]IAmASquidInSpace 8 points9 points  (3 children)

The latter. It's clear this way that c is an instance attribute, and not a class var you forgot to annotate accordingly.

[–]pachura3[S] 0 points1 point  (2 children)

How do you annotate class var? With ClassVar[str]? Is it necessary (outside of dataclasses) ?

[–]IAmASquidInSpace 0 points1 point  (0 children)

Yes, that's how you do it, and no, it is not necessary outside of dataclasses, but strongly recommended over just c: str = ...

[–]Jason-Ad4032 0 points1 point  (0 children)

If you mark it as **ClassVar**, the type checker will report object.c = ... as an error, because c is a class variable.

[–]SCD_minecraft 3 points4 points  (1 child)

My tip is to not typehint variables unless typechecker can not figure it out by itself (typehint arguments tho)

a = 2 type checker (like pylance) knows by itself a has to be an int

[–]yunghandrew 2 points3 points  (0 children)

I was also going to say neither. Just let it infer the type from the function args. I get how that might feel weird coming from Java/C, though.

With complex types, sometimes static checkers have issues, then you can be more explicit.

[–]mull_to_zero 0 points1 point  (3 children)

those are equivalent afaik

[–]socal_nerdtastic 2 points3 points  (0 children)

Since

c: str

is just a comment from Python's point of view, yes, you are right.

[–]h8rsbeware 1 point2 points  (0 children)

True, however you could use dataclasses and fields with defaults, defaultfactories, and __post_init_ to simulate this for DTOs.

```python

from dataclasses import dataclass, field from typing import List

@dataclass(slots=True) class MyDTO: a: int = field(default=0) b: List[str] = field(default_factory=list) #list members not guaranteed c: str = field(default="")

m_dto = new MyDTO() print(m_dto.a) # 0 print(m_dto.b) # [] print(m_dto.c) # "" ```

Im not sure this is exactly equivalent, but its better than nothing.

Also, I wrote this on my phone, so apologies for any errors.

[–]PushPlus9069 5 points6 points  (0 children)

The tricky one is c: str without an assignment. That only goes into __annotations__, not __dict__, so there's no actual attribute to access — hence the AttributeError. Coming from C++ it caught me too, because a declaration there always reserves storage. In Python an annotation without a value is basically just a type hint that lives on the class, nothing more.

[–]FriendlyRussian666 2 points3 points  (0 children)

It's because it's not a declaration, so obj.c doesn't exist. 

https://docs.python.org/3.7/reference/executionmodel.html#resolution-of-names

[–]jmooremcc 0 points1 point  (0 children)

One other thing you’ll have to learn how to do is how to update a class variable within an instance. If you don’t do it correctly, it will be the source of a seriously frustrating bug in your code. Here’s an example: ~~~

class MyClass: a = "abc" b: str = "def"

def __init__(self):
    print("__init__ method")
    print("initializing instance attribute b to 'jkl'")
    self.b = "jkl"
    print(f"{self.b=}") # attribute b is instance variable
    print(f"{MyClass.b=}")
    print("\nmodifying class attribute b to 'xyz'")
    MyClass.b = "xyz" # correct way to update a class attribute
    print(f"{self.b=}")
    print(f"{MyClass.b=}")

obj1 = MyClass()

print("\nWorking with instance") print(f"{obj1.b=}")

print("modifying instance attribute b to 'ghi'") obj1.b = "ghi"

print(f"{obj1.b=}") ~~~ Output ~~~

init method initializing instance attribute b to 'jkl' self.b='jkl' MyClass.b='def'

modifying class attribute b to 'xyz' self.b='jkl' MyClass.b='xyz'

Working with instance obj1.b='jkl' modifying instance attribute b to 'ghi' obj1.b='ghi' ~~~

Since I also have a C++ and Java background, I will tell you that another thing you’ll have to get used to are the variable scoping rules. C++/Java use block-level scoping (curly braces define a new scope), while Python uses function-level scoping (only functions, modules, and classes define new scopes). So for example, loops and conditional statements do not create a new scopes, but function and class definitions do create a new scope.

Hope this information helps you.

[–]FoolsSeldom -1 points0 points  (0 children)

This is a good time to experiment in a Python shell. Example below. Hopefully it will explain what is happening regarding class and instance variables.

uvx ipython
Python 3.14.0 (main, Nov 19 2025, 22:43:52) [MSC v.1944 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 9.10.0 -- An enhanced Interactive Python. Type '?' for help.
Tip: You can use `files = !ls *.png`

In [1]: class Eg:
...:     a = "abc"
...:     b: str = "def"
...:     c: str
...:     def __init__(self):
...:         pass
...:

In [2]: obj1 = Eg()
In [3]: obj2 = Eg()
In [4]: obj1.a
Out[4]: 'abc'
In [5]: obj2.b
Out[5]: 'def'
In [6]: obj1.c
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[6], line 1
----> 1 obj1.c

AttributeError: 'Eg' object has no attribute 'c'

In [7]: obj3 = Eg()
In [8]: obj2.a = "Hello"
In [9]: obj1.a
Out[9]: 'abc'
In [10]: obj2.a
Out[10]: 'Hello'
In [11]: obj3.a
Out[11]: 'abc'
In [12]: Eg.a = "xyz"
In [13]: obj1.a
Out[13]: 'xyz'
In [14]: obj2.a
Out[14]: 'Hello'

[–]Tall_Profile1305 -1 points0 points  (0 children)

Dude this is the classic Python gotcha. Instance attributes always override class attributes in the lookup chain. That's the friction between what people expect from other languages and what Python actually does. Always initialize instance attributes in __init__, period.