3 Tricky Python Nuances
Tags: python, beginners, explainlikeimfive
Published: 2019-01-16 20:57:47

---
title: 3 Tricky Python Nuances
published: true
description: Three Python oddities that can make life difficult if you're not expecting them.
tags: python, beginners, explainlikeimfive
cover_image: https://thepracticaldev.s3.amazonaws.com/i/rt7ym5f49o0gvy2p9zdp.jpg
---

This blog post is adapted from a talk I gave last week at [ChiPy](https://chipy.org), the Chicago Python User Group. For more great content like this, I highly recommend your local Python meetup, and [PyVideo](https://pyvideo.org), the community project aimed at indexing every recorded Python meetup or conference talk.


# Nuance Number One

## Lists Don't Contain Objects

I know this sounds like a ridiculous statement. Everything in Python is an object. If declare a list, I'm filling it full of objects. Collecting objects is what a list does!

Kind of. Let me back up a bit.

Over [Advent of Code](https://adventofcode.com) a couple friends and I were sharing code snippets and debugging things together in our Friendship Discord. One of the problems involved creating a giant matrix that represented cloth, and you had to figure out which squares of cloth were being used by manipulating individual elements in that matrix. I have a friend who is learning Python right now, and setting the problem up turned out to be [more difficult than they thought](https://dev.to/rsdesoto/til-python-comprehensions-make-matrices-work-1ge1).

Here's how they set up their matrix initially. Simple enough, right?

```python

l = [[0] * 5] * 5

print(l)
[[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]]
```

So far so good. But when they went to change an element in the first nested list...

```python
l[0][3] = 1

print(l)
[[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0],
[0, 0, 0, 1, 0]]
```

😧

They posted in our discord group, asking what the heck was going on. My answer, as usual, was pretty much to shrug and ask why they weren't using a list comprehension. Another of our friends said something to the effect of "seems like it could be pointers?"

My second friend was right. Python doesn't store actual objects inside a list. It stores `references` to objects. And it's hella weird.

A `reference` points to a specific location in memory. That specific location contains the object, and if you change the object stored at that location in memory, you change every instance where it appears.

So each of the five lists inside the original matrix declared by `l = [[0] * 5] * 5` are actually just five separate `references` to the _same_ list. When you change one, you change them all.

### So wait, how do I make them different objects? 

Great question! In order to accomplish the goal of having a matrix where each element can be manipulated independently without inadvertently affecting other elements, we need to make sure we're creating a new object at a new location in memory for each nested list. In this case, we can use the copy library to create a new container object that we can change independently.

```python
from copy import copy
a = [0, 0, 0, 0, 0]

b = []

# intentionally verbose example to illustrate a point :)
for _ in range(5):
    b.append(copy(a))
```

But of course, we can make this much cleaner. I was right the first time. Mooooooost of your Python problems can be solved with a well placed list comprehension (satisfaction not guaranteed).

```python
l = [[0 for i in range(5)] for j in range(5)]

l[0][3] = 1

print(l)

[[0, 0, 0, 1, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]]
```

# Nuance Number Two
## Default arguments are evaluated when the function is defined, _not_ each time a function is called

What does this mean? It's easier to explain through an example. Let's pretend that, for some reason, we want to write a function that takes in an element and appends it to an existing list without using the `append` function.

```python
def list_append(element, input_list=[]):
    input_list.extend([element])
    return input_list
```

Now, this is a completely contrived example that we would never implement in practice, but it's useful for demonstration. Let's try calling that function a few times.

```python
print(list_append(3))
[3]
print(list_append([5, 7]))
[3, 5, 7]
print(list_append("asdf"))
[3, 5, 7, "asdf"]
```

When we call the function multiple times, it seems that it's always adding to and returning the same object, rather than starting with a fresh empty list each time the function is called.

This is because the default argument for a function is created and evaluated when the function is first declared. So it would get evaluated and you'd start with a fresh list every time you restarted your app, for example. But each time your function is called while the app is running it's going to be working off of the same mutable object, and will just keep growing and growing until the app is restarted again.

Here's how we fix this:

```python
def list_append(element, input_list=None):
    if not input_list:
        input_list = []
    return input_list.extend([element])

print(list_append(3))
[3]
print(list_append([5, 7]))
[5, 7]
print(list_append("asdf", ["bcde"]))
["bcde", "asdf"]
```

Elements declared within a function's scope are evaluated every time the function runs, so in the above example you'll be starting with a new empty list every single time, instead of passing the same default argument from function call to function call.

This isn't just a problem with lists, by the way. You should always be careful about passing in mutable objects as default arguments to a function; all mutable objects will have this same problem.

# Nuance Number Three
## `==` vs `is`

`is` and `==` are both operators in Python that evaluate two objects against one another and return a boolean value based on the results. This is a little tricky, but `==` checks for _equality_ while `is` checks for _identity_.

Remember back to nuance number one when we talked about `references`, or specific locations in memory that contain an object? They're important again here!

_Equality_ means that the values of two objects match. _Identity_ means that the two things you're comparing are the _exact same object_ and exist at the same location in memory. You can check the location of an object in memory by using the builtin Python function `id()`. So when you compare two objects using the `is` operator, you're checking that the two objects have the same `id`, not the same `value`.

A quirk of this, that you'll often see in ideomatic Python, is that there is only one `True` boolean object and only one `False` boolean object in memory. Each appearance of `True` has the same id as every other appearance of `True`. However, there are some values that evaluate as being _equal_ to `True` without sharing its identity. That's why Pythonistas often use the `is` operator when comparing things to `True` or `False` rather than the `==` operator.

This can be a bit confusing and jargon filled when explained in words. Here are some examples.

### Equality vs identity for boolean values:
```python
print(1 == True)
True
print(1 is True)
False
a = True
print(a is True)
True


print(0 == False)
True
print(0 is False)
False
b = False
print(b is False)
True
```

### Equality vs identity - equivalent variables
```python
# a and b variables point to the same id
a = [1, 2, 3, 4, 5]
b = a
print(a == b)
True
print(a is b)
True
print(id(a), id(b))
4437740104, 4437740104

# a and b are different container objects, but the values of their contents are identical
a = [1, 2, 3, 4, 5]
b = [1, 2, 3, 4, 5]
print(a == b)
True
print(a is b)
False
print(id(a), id(b))
4437740104, 4442640968
```

# Conclusion

I'm not sure I have a unified conclusion here. These are just three things that might trip you up in Python that are hard to google for, since they lead to unexpected behaviors but don't give you a nice error message.

I suppose if I were to give you a takeaway it's that
1.) `references` are kind of wonky. You usually don't have to think about them, but they're worth understanding
2.) Always use list comprehensions

As always, let me know if you have any questions in the comments, or if you think I missed anything important regarding these three topics. Let me know if there's a Python quirk you don't get and would like an explainer on. Thanks!