Python for AIPython 3.12 · Pydantic 2.12
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
43 small wins to finish your pathNext lesson →

Common gotchas

A gotcha is a piece of correct-looking Python that does something other than what you expect; this lesson covers the three that catch almost everyone: is versus ==, copying a nested list, and a mutable default argument.

Last updated: 13 Oct, 2026 · Python 3.12

The Decorators lesson leaned on one such trap: a wrapper that returns the wrong thing. This lesson collects three more that bite in real code, each shown with the output that gives it away, then the fix.

is checks identity, == checks value

== asks whether two values are equal. is asks whether they are the same object in memory. Two lists can be equal without being the same object, so the two questions give different answers.

What the is keyword checks · from Python Series- is Vs == Indepth Explanation- Boolean Evaluation · 3:10 to 5:00
From the video. The clip shows the result of each line in a Jupyter notebook without print(). The runs here wrap the result in print() so the same comparisons show their values in a .py file.
ExampleEqual lists are not the same object
list1 = [1, 2, 3, "krish"]
list2 = [1, 2, 3, "krish"]      # a second list with the same values

print(list1 == list2)           # equal contents?
print(list1 is list2)           # the same object in memory?

list2 = list1                   # now list2 is another name for list1
print(list1 is list2)
  • list1 == list2 is True: the two lists hold equal values.
  • list1 is list2 is False: they are two separate objects in memory, built one after the other.
  • After list2 = list1, list1 is list2 is True: the assignment did not copy; it gave the one object a second name, so both point at it.

The rule of thumb: compare values with ==, and keep is for checking against None, as in if result is None. None is a single object, so is is exactly right there.

A shallow copy shares the inner lists

Copying a list of lists has a trap. The list's own .copy() makes a shallow copy: a new outer list whose slots point at the very same inner lists. copy.deepcopy makes a deep copy: new inner lists too, sharing nothing.

Shallow copy versus deep copy of the nested list [[1, 2], [3, 4]]. The original points to two inner lists. A shallow copy made with copy.copy() is a new outer list whose two slots point to the very same inner lists as the original, so changing an inner list changes both. A deep copy made with copy.deepcopy() builds a new outer list and new inner lists, so it shares nothing with the original.
A shallow copy shares the inner lists · from Shallow Copy Vs Deep Copy in Python · 3:36 to 6:47
From the video. The clip takes a shallow copy with the list's own .copy() method, then changes an element of a nested inner list and shows the copy changing too. The run below does the same with list1 and list2, and adds copy.deepcopy as the fix.
ExampleChanging an inner list reaches the shallow copy
import copy

list1 = [[1, 2], [5, 6, 7, 8]]
shallow = list1.copy()          # his .copy(): a shallow copy
deep = copy.deepcopy(list1)     # a deep copy, with new inner lists too

list1[1][0] = 100               # change an inner list of the original
print("shallow:", shallow)
print("deep:", deep)
  • shallow: [[1, 2], [100, 6, 7, 8]]: the change to the original's inner list showed up in the shallow copy, because both point at the same inner list.
  • deep: [[1, 2], [5, 6, 7, 8]]: the deep copy was untouched, because it has its own inner lists.
  • list(x) and x[:] also make shallow copies, so they have the same trap with nested data.

This bites with lists of dictionaries, the chat-messages shape from the Lists of dictionaries lesson. A shallow copy of the message list shares the message dicts, so editing one edits the original conversation too.

ExampleA shallow copy of messages still shares the dicts
messages = [{"role": "user", "content": "hi"}]
shallow = list(messages)
shallow[0]["content"] = "changed"
print(messages)

Editing the dict through shallow changed the dict inside messages, because both lists hold the same dict. Use copy.deepcopy when you need to edit a copy of nested data and leave the original alone.

A mutable default argument is created once

A default value like basket=[] is built one time, when the function is defined, and reused by every call that does not pass its own. So the list fills up across calls instead of starting empty each time.

ExampleThe default list is shared across calls
def add_item(item, basket=[]):
    basket.append(item)
    return basket

print(add_item("apple"))
print(add_item("banana"))
print(add_item("cherry"))

Each call appended to the same list, so it grew. The fix is to default to None and build a fresh list inside the function.

ExampleA None default gives every call its own list
def add_item(item, basket=None):
    if basket is None:        # a fresh list when none is given
        basket = []
    basket.append(item)
    return basket

first = add_item("apple")
second = add_item("banana")
print(first, second)

Each call with no basket now started from an empty list, so first is ['apple'] and second is ['banana'], with nothing carried between them. The is None check is the right use of is from the first section.

is vs ==

is==
Comparesidentity, the same objectvalue, equal contents
Two equal listsFalse, separate objectsTrue
Keep it forchecking against Nonecomparing values

Where these gotchas show up in AI code

  • Checking a parsed field against None with is, and comparing two payloads for equality with ==.
  • Copying a list of chat messages before editing one, with copy.deepcopy, so the stored conversation is left untouched.
  • A function that collects results into a default list and surprises you by keeping the previous call's results.
Watch out. The mutable default is the quiet one. The empty list in def f(x, acc=[]) is made once and shared by every call, so results leak between calls and the bug shows up far from its cause. Default to None and build the list inside. The same holds for a default dict or set.
Try it yourself
  • Change list(messages) to copy.deepcopy(messages) and confirm the original messages is left unchanged.
  • Call the basket=[] version four times and watch the list keep every item; then switch to the None version.
  • Make two lists with the same contents and print a is b (False) and a == b (True); then set c = a and print c is a (True).
PreviousDecorators

Little by little, you're building something great.