October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk7 min

10 Python Variable Mistakes Developers Still Make (And How to Fix Them)

Most Python variable bugs come from treating names as copies instead of labels on objects. Here are ten common mistakes, with code and fixes.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Most Python variable bugs come from one misunderstanding: a name is a label attached to an object, not a box that holds its own copy of the data. Once that model is clear, the mistakes below stop looking random. The examples target Python 3. The list reflects common pitfalls that show up repeatedly in code and in the official documentation, arranged by theme. It is not a ranking by how often each mistake occurs.

The model behind most variable bugs

An assignment such as b = a binds the name b to the object that a already refers to. It does not create a new list, dictionary, or any other object. The Python Tutorial states the principle directly: assignments do not copy data; they bind names to objects. The Python Programming FAQ applies the same idea to function calls with the sentence “Remember that arguments are passed by assignment in Python.” Inside a function, the parameter name is bound to the object the caller passed in, so changes made to a mutable object are visible to the caller.

Two consequences explain most of the mistakes below. First, two names can refer to one mutable object, and changing the object through either name changes it for both. Second, rebinding a name changes only that name’s binding, leaving other names untouched. Whether a line of code mutates an object or rebinds a name is the detail that decides the outcome.

Mistakes with shared objects

1. Assuming assignment copies a list

This is the most common surprise for newcomers coming from languages where assignment copies values:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
a = [1, 2, 3]
b = a
b.append(4)
print(a)   # [1, 2, 3, 4]
print(a is b)   # True

Both names point to the same list, so append changes the one object they share. When you need independent state, make a copy. For a list, b = a.copy(), b = list(a), and b = a[:] each create a new top-level list. The copy is shallow. Nested objects inside the list are still shared:

nested = [[1], [2]]
clone = nested.copy()
clone[0].append(9)
print(nested)   # [[1, 9], [2]]

If the structure contains mutable objects that must be independent, you need a deeper copy. The standard library’s copy module provides copy.deepcopy() for that case.

2. Confusing rebinding with mutation

Operators that look alike can behave differently. For lists, += extends the existing object in place, while + builds a new list. The effect is visible when another name shares the object:

a = [1, 2]
b = a
b += [3]        # extends the shared list in place
print(a)        # [1, 2, 3]

b = b + [4]     # builds a new list and rebinds b only
print(a, b)     # [1, 2, 3] [1, 2, 3, 4]
Operation (lists) Mutates the shared object? Effect on other names pointing to the original list
a.append(4) Yes Visible through every name
a += [4] Yes, the list is extended in place Visible through every name
a = a + [4] No, a new list is created and a is rebound Other names keep the old list
b = a.copy() No, a new top-level list is created Later changes to b do not affect a, though nested objects remain shared

For immutable types such as integers and strings, += always produces a new object and rebinds the name. The mutation-versus-rebinding distinction matters mainly for mutable types.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

3. Using a mutable default argument as per-call storage

Default values are evaluated once, when the def statement runs, not each time the function is called. A mutable default is therefore shared across every call:

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("a"))   # ['a']
print(add_item("b"))   # ['a', 'b']  -- state carried over from the first call

The fix is a None sentinel, with the fresh mutable object created inside the function:

def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

print(add_item("a"))   # ['a']
print(add_item("b"))   # ['b']

Use this pattern when each call should start clean. If you intentionally want a shared cache, make that explicit with a clearly named module-level object instead of a hidden default.

Mistakes with scope

4. Expecting a function assignment to update a global variable

total = 0

def set_total():
    total = 10      # creates a local name; the global is untouched

set_total()
print(total)        # 0

Any assignment to a name inside a function makes that name local to the function, unless a global or nonlocal declaration says otherwise. The usual fix is to return the value and assign it at the call site:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
def compute_total():
    return 10

total = compute_total()
print(total)        # 10

If module-level state is truly intended, declare it with global total inside the function. Returning values is usually easier to test and reason about, and the Python Programming FAQ recommends returning results as a clear option for output-style functions.

5. Reading a local name before it is assigned

count = 0

def bump():
    print(count)    # raises UnboundLocalError
    count += 1

bump()

The function contains an assignment to count, so Python treats count as local throughout the entire function body. The read on the first line looks up a local that has no value yet, which raises UnboundLocalError. The exact message text varies between Python versions, but the exception type is the same. The reference for this behavior is in the Python execution model documentation.

Two fixes are common. Pass the value in and return the new one:

def bump(count):
    return count + 1

count = bump(count)

Or declare the intended outer binding with global count, if the function really should change module state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

6. Using global or nonlocal without knowing which binding changes

The two keywords look similar but target different scopes. global refers to the module’s global namespace. nonlocal refers to a name in the nearest enclosing function scope, and it cannot point at a module-level name. The simple statements reference covers assignment and its effect on names:

def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter = make_counter()
print(counter(), counter())   # 1 2

Without nonlocal, the line count += 1 would make count local to increment and fail. Before reaching for either keyword, ask whether explicit parameters and return values would make the dependency visible. Often they do.

Mistakes with closures and comprehensions

7. Capturing a changing loop variable in a lambda or nested function

A closure looks up its free variables when it is called, not when it is created. Every lambda below therefore sees the final value of i:

funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])   # [2, 2, 2]

Bind the current value as a default argument, which is evaluated at creation time:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)

print([f() for f in funcs])   # [0, 1, 2]

A small factory function achieves the same result and reads more clearly when the closure body is longer:

def make_printer(value):
    return lambda: value

funcs = [make_printer(i) for i in range(3)]
print([f() for f in funcs])   # [0, 1, 2]

8. Assuming a comprehension variable behaves like a loop variable

In Python 3, the iteration variable of a comprehension stays inside the comprehension. The loop variable of a for statement does not. Mixing those two cases up leads to confusion about what a name holds afterward:

x = "before"
values = [x for x in range(3)]
print(x)          # before

for x in range(3):
    pass
print(x)          # 2

Assignment expressions are the exception that needs attention. A name bound with := inside a comprehension is bound in the containing scope, as PEP 572 specifies:

squares = [last := n * n for n in range(4)]
print(last)       # 9

The rule depends on the construct, so avoid generalizing from one case to another. Python 2 behaved differently for list comprehensions, which is why older code sometimes depends on leaked loop variables.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Mistakes that make names misleading

9. Shadowing a built-in name such as list

Python looks up names through local, enclosing, global, and built-in scopes. If you bind a built-in name in your own scope, your binding wins:

list = [1, 2, 3]
numbers = list("abc")   # TypeError: 'list' object is not callable

The error message points at the call, not at the earlier assignment that caused it. Choose names such as values or items. If you find a shadowed built-in in old code, search for the name’s assignments first, not just the line that fails.

10. Reusing one variable for unrelated meanings or types

Python permits rebinding a name to a value of any type, so this is not a runtime error. It is a readability problem. A name that holds raw text, then a parsed dictionary, then a validated object forces the reader to track which meaning applies at each line:

data = read_file(path)        # str
data = json.loads(data)       # dict
data = validate(data)         # still a dict, but now checked

Distinct names make each stage visible: raw_text, parsed, and config. The Hitchhiker’s Guide to Python offers general style guidance on this kind of repeated reassignment. Treat it as a maintainability practice rather than a language rule.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A checklist when a value changes unexpectedly

  • Check whether two names refer to the same object. a is b returns True when they do, and id(a) shows the underlying identity.
  • Look for the assignment that changed the value. Search the function for =, +=, for, import, and :=. Any of these can make a name local.
  • Inspect mutable default arguments in the function signature.
  • If a lambda or nested function returns an unexpected value, check whether it captures a loop variable.
  • If you get UnboundLocalError, find the assignment to that name in the same function and decide whether it should be a parameter or an intentional global.

Most of these checks reduce to one question: which object does this name refer to right now, and which lines change that object or the name’s binding?

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.