![Python Oddities That Might Surprise You](https://www.kdnuggets.com/wp-content/uploads/PYTHON-ODDITIES.png)
Image by Author | Canva
Have you ever come across something quirky in Python that led to one of those “wait, what?” moments?
Yes, Python is known for its readability and simplicity, but it has its share of surprising behaviors that can catch even some experienced developers off guard.
Let’s explore some of these together and understand why they happen.
▶️ Get the Google Colab notebook with all the example code.
1. Mutable Default Arguments
Mutable default arguments in functions is a common Python gotcha!
I’ve written about it previously in other articles. But in an article on Python oddities, we cannot exclude it, can we? ☺️
So yeah. Here’s something that might surprise you:
def add_item(item, list_of_items=[]):
list_of_items.append(item)
return list_of_items
Just so you can follow easily, I’ll put the function calls and the outputs in a single code block.
print(add_item(1))
Output >>> [1]
print(add_item(2))
Output >>> [1, 2]
Why does this happen? The key is understanding when Python evaluates default arguments.
🔖 Default arguments are evaluated when the function is defined, not when it’s called.
This means the empty list []
is created once when the function is defined, and that same list is reused in every function call where a second argument isn’t provided.
Here’s how to fix this:
def add_item(item, list_of_items=None):
if list_of_items is None:
list_of_items = []
list_of_items.append(item)
return list_of_items
In this corrected version, we use None
as our default argument and create a new list each time the function is called.
print(add_item(1))
Output >>> [1]
print(add_item(2))
Output >>> [2]
2. Late Binding Closures
Here’s a tricky one that often appears in loops:
functions = []
for i in range(3):
functions.append(lambda: i)
print([f() for f in functions])
Here’s what you get:
You might expect this to print [0, 1, 2]
, but it doesn’t. Why? This behavior is called “late binding,” and it’s related to how Python handles closures.
▶️ When the lambda function references the variable i
, it doesn’t capture the value of i
at the time the function is created – instead, it captures the variable itself.
Let’s take an analogy.
Think of it like this: imagine you’re writing a note that says “check the number on the whiteboard.” When you later read the note, you’ll look at whatever number is currently on the whiteboard, not the number that was there when you wrote the note, yes? In our code, by the time we call these functions, the loop has finished and i
is left at its final value (2).
Here’s how to get the behavior you probably wanted:
functions = []
for i in range(3):
functions.append(lambda x=i: x) # Using default argument to capture current value
print([f() for f in functions])
By using a default argument, we’re effectively taking a snapshot of i
‘s value at the time each function is created.
3. Identity vs. Equality
Python’s identity and equality operators can sometimes surprise you (especially if you’re new to Python).
# Integer caching
a = 256
b = 256
print(a is b)
c = 257
d = 257
print(c is d)
What’s going on here? This behavior is related to Python’s memory optimization features. For small integers (typically -5 to 256), Python caches the objects and reuses them.
For strings, Python uses a technique called “string interning” where it might reuse string objects to save memory. However, this behavior is not guaranteed for all strings and can vary between Python implementations.
# String interning
x = "hello"
y = "hello"
print(x is y)
p = "hello!"
q = "hello!"
print(p is q)
Output >>> False (check at your end!)
This is why you should always use:
-
==
for comparing values is
only for comparing withNone
or checking if two variables reference the exact same object
The is
operator checks if two variables refer to the exact same object in memory, while ==
checks if two objects have the same value.
4. Variable Unpacking Surprises
Unpacking operations can sometimes be confusing if you aren’t as comfortable with how Python unpacks. Let’s get straight to examples.
This works as expected:
This also works:
a, *b = 1, 2, 3, 4
print(a, b)
But this might surprise you:
And this is valid too:
(*a,) = [1, 2, 3]
print(a)
So here’s what you should know: The asterisk operator (*) in unpacking collects multiple values into a list, even if there are no values to collect!
5. List Multiplication (Not Always What You Expect!)
At first look, multiplying a list by a number seems straightforward, but it can lead to some surprising behaviors, especially with nested lists.
Let’s take a simple example first:
# Simple list multiplication
simple_list = [1] * 3
print(simple_list)
Now look at the following examples with nested lists:
nested_list = [[1, 2]] * 3
print(nested_list)
Output >>> [[1, 2], [1, 2], [1, 2]]
nested_list[0][0] = 5
print(nested_list)
Can you guess the output?
Output >>> [[5, 2], [5, 2], [5, 2]]
What’s happening here? When you multiply a list, Python does not create deep copies of the elements – it creates multiple references to the same objects. So when you modify the inner list at nested_list[0][0]
, you’re modifying the single inner list that all three elements reference.
Here’s one way you can create truly independent copies:
nested_list = [[1, 2] for _ in range(3)]
nested_list[0][0] = 5
print(nested_list)
The list comprehension creates new inner lists for each element.
Output >>> [[5, 2], [1, 2], [1, 2]]
Wrapping Up
If you take a closer look at all we’ve covered, you’ll realize this: these aren’t surprises at all if you know how Python works. So understanding them should likely help you:
- Write more reliable code by avoiding common pitfalls
- Better understand Python’s internal workings
- Debug issues more effectively
In my Python gotchas article, I discuss a few other such oddities. So yeah, keep learning and coding!
Bala Priya C is a developer and technical writer from India. She likes working at the intersection of math, programming, data science, and content creation. Her areas of interest and expertise include DevOps, data science, and natural language processing. She enjoys reading, writing, coding, and coffee! Currently, she’s working on learning and sharing her knowledge with the developer community by authoring tutorials, how-to guides, opinion pieces, and more. Bala also creates engaging resource overviews and coding tutorials.