Advanced Python mistakes are the ones that survive code review because the code looks correct. The program runs without crashing, but it produces wrong results, leaks memory, or behaves inconsistently. These mistakes come from misunderstanding Python's evaluation model rather than from typos or syntax errors.
The articles throughout this section covered many of the individual topics: the object model, descriptors, metaclasses, and memory management. This article collects the mistakes that experienced developers make when combining these features, along with the fixes that prevent them.
Each mistake reveals something fundamental about Python. Understanding why the mistake happens teaches you more than memorizing the fix.
Mutable default arguments
This is the most famous Python gotcha, and it still catches experienced developers. Default argument values are evaluated once at function definition time, not each time the function is called.
def append_to(item, target=[]):
target.append(item)
return target
print(append_to(1))
print(append_to(2))
print(append_to(3))The same list object is shared across every call to the function. Each invocation appends to the accumulated list from all previous calls that relied on the default argument value.
[1]
[1, 2]
[1, 2, 3]The fix is to use None as the sentinel default and create a new mutable object inside the function body only when needed.
def append_to(item, target=None):
if target is None:
target = []
target.append(item)
return target
print(append_to(1))
print(append_to(2))Each call now gets its own fresh list. The None sentinel pattern is the standard solution and works for lists, dicts, sets, and any other mutable type.
[1]
[2]Late-binding closures in loops
Closures capture variables by reference, not by value. When you create closures inside a loop, every closure references the same loop variable, which holds its final value after the loop completes, a detail the article on Python namespace and scope internals explains through cell objects.
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs])All three closures return 2 because that is the value of i after the loop finishes. Each closure captured the variable, not its value at creation time.
[2, 2, 2]The fix captures the current value at each iteration by binding it as a default argument value to the lambda parameter. Default arguments are evaluated at definition time rather than at call time, creating distinct closures for each iteration.
funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
print([f() for f in funcs])Default arguments are evaluated at definition time. The i=i expression captures the current value of i as the default for the parameter, producing three distinct closures.
[0, 1, 2]Metaclass overuse
Metaclasses are powerful, but most problems that seem to need a metaclass can be solved with simpler tools. The init subclass hook handles subclass registration, class decorators handle post-creation modification, and inheritance and mixins handle shared behaviour.
Before writing a metaclass, ask whether a class decorator would work, and use it if the answer is yes. If you only need to react to new subclasses, use init subclass instead. The article on Python metaclasses covers the legitimate use cases.
The metaclass is the right tool only when you need to intercept the class creation process itself. This means validating or modifying the namespace before the class object exists, or controlling the type of the resulting class.
Shadowing built-in names
Using built-in names as variable names works at first but breaks when you later need the built-in. The names list, dict, set, str, id, type, and filter are commonly shadowed.
list = [1, 2, 3]
try:
more = list((4, 5))
except TypeError as e:
print(e)The variable list shadows the built-in list type. Subsequent calls to the list constructor fail because the name now refers to a list instance.
'list' object is not callableThe fix is to use descriptive variable names: instead of list, use items, values, or results, and instead of id, use identifier or object_id. Never shadow the name type, which is needed for both introspection and metaclass work.
Circular imports
When two modules import from each other, Python can produce confusing AttributeError messages. Module A imports from B, B starts importing from A, but A has not finished loading yet, so the name B wants is not yet defined.
The best fix is to restructure the code to break the cycle. Move shared dependencies to a third module that both A and B import. If restructuring is not possible, move the import inside the function that needs it rather than at module level.
def process():
from module_b import helper
return helper()The deferred import delays the resolution until the function is actually called, by which time both modules have finished loading. This is a workaround, not a solution, so prefer restructuring the modules to eliminate the cycle.
Rune AI
Key Insights
- Mutable default arguments are evaluated once and shared across calls; use None and create fresh objects.
- Closures capture variables by reference; use default arguments to capture values in loops.
- Metaclasses should be the last tool you reach for, not the first.
- Never shadow built-in names like list, dict, or id with your own variables.
- Import loops cause hard-to-debug AttributeErrors; restructure modules to break cycles.
Frequently Asked Questions
Why do mutable default arguments cause bugs in Python?
What is the late-binding closure problem?
Conclusion
Advanced Python mistakes are subtle because the code often looks correct and may even pass simple tests. Mutable defaults, late-binding closures, and metaclass overuse are the most common. The antidote is understanding Python's evaluation model: when things run, how names are resolved, and which objects are shared. Each mistake teaches something fundamental about how Python works.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.