Common Python Function Mistakes

Learn the most common mistakes Python developers make with functions, from mutable defaults to late binding closures, and how to avoid or fix each one.

7 min read

Every Python developer, from beginners writing their first functions to experienced engineers maintaining large codebases, encounters a recurring set of function-related mistakes. These are not random errors caused by carelessness. They are predictable consequences of Python's design decisions around default argument evaluation, variable scoping, closure semantics, and the distinction between returning and printing. Understanding why each mistake happens, rather than just memorizing the fix, is what prevents you from making it again in a slightly different context.

This article collects the most common function mistakes covered across this entire Functions section, consolidating them into a single reference. Some of these you have seen in earlier articles on default parameter values, closures, and passing objects to functions. Seeing them together, with clear before-and-after examples, reinforces the patterns and helps you recognize them in your own code.

Mutable default arguments

The most notorious Python function mistake is using a mutable object as a default parameter value. Because default values are evaluated once when the function is defined, all calls that use the default share the same mutable object. A function that appends to a default list accumulates items across calls:

pythonpython
# Wrong: the same list persists across every call
def add_item(item, items=[]):
    items.append(item)
    return items
 
# Right: create a fresh list inside the function body
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

The fix is mechanical: use None as the default and create the mutable object inside the function body. This pattern works for lists, dictionaries, sets, and any other mutable type. The same issue applies to default values that call functions returning mutable objects or time-sensitive values. A default like created=datetime.now() freezes the creation timestamp to when the function was defined. Use None and call the function inside the body instead.

Late binding in closures and loops

When closures are created inside a loop and capture the loop variable, all closures see the variable's final value because the capture is by reference, not by value. This is not a bug but a consistent application of Python's scoping rules:

pythonpython
# Wrong: every lambda captures the same final i
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])  # [2, 2, 2]
 
# Right: bind the current value as a default argument
funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])  # [0, 1, 2]

All closures created in a loop capture the loop variable by reference, so they all see its final value. The fix binds the current value as a default argument because default arguments are evaluated at definition time. Writing lambda i=i: i instead of lambda: i inside a list comprehension captures each iteration's value. This pattern works identically for def functions created in loops and for lambdas. Understanding the underlying cause, variables are captured by reference, is more valuable than memorizing the fix for loops specifically.

Confusing return with print

A function that uses print instead of return produces visible output but does not make that output available to the code that called it. The function returns None, and any variable that captures the call result gets None instead of the expected value:

pythonpython
# Wrong: prints the sum but returns None
def add(a, b):
    print(a + b)
 
total = add(2, 3)
print(total)  # None
 
# Right: returns the sum for the caller to use
def add(a, b):
    return a + b
 
total = add(2, 3)
print(total)  # 5

A function that uses print instead of return displays output but returns None, making the result useless for further computation. The correct approach is to return the computed value, letting the caller decide whether to print it or use it in further calculations. Mixing both in the same function creates confusion about the function's purpose and makes it difficult to test or reuse. The article on return values in Python functions covers this distinction in detail with examples of when each approach is appropriate.

Unintended local variables from assignment

Assigning to a variable anywhere inside a function marks it as local for the entire function body, even if the assignment comes after a line that tries to read the variable. This causes an UnboundLocalError when the read happens before the assignment:

pythonpython
# Wrong: Python marks count as local, read fails before assignment
def increment():
    count = count + 1  # UnboundLocalError
 
# Right: use global to reference the module-level variable
def increment():
    global count
    count = count + 1

The same issue occurs with enclosing scope variables in nested functions, where nonlocal is the fix instead of global. The article on local and global variables in Python explains the compile-time scoping decision that causes this behavior and the alternatives to using global and nonlocal.

Shadowing built-in function names

Defining a function or variable with the same name as a Python built-in shadows the built-in within that scope. Code that expects the standard len or list behavior gets your custom version instead, which can break library code that you import and that depends on the built-ins working correctly:

pythonpython
# Wrong: shadows the built-in list type
def list(items):
    return [x for x in items if x is not None]
 
# Right: use a distinct name
def filter_none(items):
    return [x for x in items if x is not None]

The fix is simple: choose a name that does not conflict with a built-in. If you must shadow a built-in, access the original through the builtins module, and document the shadowing prominently. The article on built-in functions versus user-defined functions covers this and other interactions between your functions and Python's built-in vocabulary.

Rune AI

Rune AI

Key Insights

  • Never use mutable objects as default parameter values; use None and create the object inside the function.
  • Closures capture variables by reference, not by value; fix late binding in loops with default arguments.
  • Confusing return with print is a common beginner error; return sends a value to the caller, print displays it.
  • Assigning to a variable inside a function makes it local; use global or nonlocal to modify outer scope variables.
  • Shadowing built-in names like len or list with your own variables or functions causes confusing bugs.
RunePowered by Rune AI

Frequently Asked Questions

What is the most common mistake in Python function definitions?

Using a mutable object like an empty list or dictionary as a default parameter value. Because default values are evaluated once at definition time, all calls share the same mutable object, leading to accumulated state across calls. The fix is to use None as the default and create the mutable object inside the function body.

Why does my function return None when I expected a value?

The function is either missing a return statement or the return statement is inside a conditional that was not satisfied. A function without an explicit return returns None by default. Also check that you are not confusing return with print: print displays output but does not return a value, so the function still returns None.

Why does my function not see a variable I defined outside it?

If the function assigns to the variable anywhere in its body, Python marks it as local for the entire function. Reading the variable before the assignment raises an UnboundLocalError. The fix depends on intent: use the global keyword to reassign a global variable, pass the value as a parameter instead, or restructure the code to avoid the reassignment.

Conclusion

Every Python developer encounters these mistakes, and recognizing them is the first step to avoiding them. Mutable default arguments, late binding closures, shadowed built-ins, and the return-versus-print confusion are all predictable consequences of Python's design choices, and the fixes are mechanical and reliable. Learning these patterns now saves you from debugging them in production later.