DEV Community

Cover image for I Asked 20 Python Developers to Trace This Code. 18 Got It Wrong.
Ameer Abdullah
Ameer Abdullah

Posted on

I Asked 20 Python Developers to Trace This Code. 18 Got It Wrong.

I shared this code with 20 developers ranging from students to people with three or more years of experience. I asked them to write down the output before running it.

18 of them got it wrong.

class Config:
    settings = {}

    def add(self, key, value):
        self.settings[key] = value

c1 = Config()
c2 = Config()

c1.add("theme", "dark")
c2.add("language", "Python")

print(c1.settings)
print(c2.settings)
Enter fullscreen mode Exit fullscreen mode

Take a moment. What do you think both lines print?


What Most People Predicted

The most common answer was:

{'theme': 'dark'}
{'language': 'Python'}
Enter fullscreen mode Exit fullscreen mode

The logic: c1 and c2 are separate objects, so their settings should be separate.


What It Actually Prints

{'theme': 'dark', 'language': 'Python'}
{'theme': 'dark', 'language': 'Python'}
Enter fullscreen mode Exit fullscreen mode

Both instances share the same dictionary. Adding to c2 also affected c1.


Why This Happens

settings = {} is a class variable. It is defined on the Config class itself, not on any instance.

When you write self.settings[key] = value, Python looks up self.settings. It checks the instance's __dict__ first. Finding no settings attribute there, it looks at the class. It finds Config.settings which is the shared dictionary.

It then calls dict.__setitem__ on that shared dictionary. This mutates the shared object. Both c1 and c2 read from the same dictionary so both see all additions.


The Fix: Move to init

class Config:
    def __init__(self):
        self.settings = {}

    def add(self, key, value):
        self.settings[key] = value

c1 = Config()
c2 = Config()

c1.add("theme", "dark")
c2.add("language", "Python")

print(c1.settings)
print(c2.settings)
Enter fullscreen mode Exit fullscreen mode

Output:

{'theme': 'dark'}
{'language': 'Python'}
Enter fullscreen mode Exit fullscreen mode

Moving settings = {} into __init__ creates a new dictionary for each instance. self.settings in __init__ creates an instance attribute that shadows any class attribute of the same name.


Why This Pattern Appears in Interviews

This question reveals whether a candidate understands the difference between class attributes and instance attributes at the level of Python's actual object model.

Writing a class is easy. Understanding how Python resolves attribute access through the instance's __dict__ first and then the class is the underlying knowledge being tested.

The follow-up question is always: "What if settings were a string instead of a dict? Would the behavior be different?"

class Config:
    name = "default"

    def set_name(self, new_name):
        self.name = new_name

c1 = Config()
c2 = Config()

c1.set_name("custom")
print(c1.name)
print(c2.name)
Enter fullscreen mode Exit fullscreen mode

Output:

custom
default
Enter fullscreen mode Exit fullscreen mode

With a string, self.name = new_name creates a new instance attribute on c1 that shadows the class attribute. c2 has no instance attribute so it still reads the class attribute "default".

With a dict, self.settings[key] = value mutates the existing class attribute through the reference. It never creates an instance attribute.

Mutation versus rebinding. The same distinction that explains list and integer += behavior.

For more class tracing problems, try PyCodeIt.


Top comments (1)

Collapse
 
algorhymer profile image
algorhymer

This demonstrates that a vague scripting language and a pseudo-intellectual play-doh philosophy like OO result in code which is hard to maintain.
Essentially it results in a workflow, where most of your time is spent - instead of constraints, invariants, or completeness - on duct taping the plumbing, and understanding how the other blue collar who last touched the code forced his/her will through the machinery with dirty hacks, leaving a trace of landmines behind.

But... it can get worse... much worse.
Take a look at Ruby. Especially meta programming. Now that is how you can write truly utterly bad code. I think dev.to is partially Ruby too.
You know... once you see syntax error code actually running in prod... that is when you start understanding what Ruby on Rails really is.