Python Oddities That Might Surprise You


Python Oddities That Might Surprise You
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 with None 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.



Recent Articles

Related Stories

Leave A Reply

Please enter your comment!
Please enter your name here