Emmanuel Akinfulubi

Tracebacks

The errors you are actually going to hit, in the interpreter's own words. 36 entries across 25 exception types.

Every traceback on this page is real. Each snippet declares the exception it expects, and the build runs it and checks: if it does not raise, or raises something else, the build fails and this page is not written. So what you see under each snippet is what CPython actually printed for exactly that code. Two things are normalised so the output is the same on any machine — the temporary directory each snippet ran in becomes example.py, and the interpreter's install location becomes <python>. The type, the message, the line numbers and the ^^^ markers are untouched.

It will not even start

These are raised before any of your code runs, while Python is still reading the file. Nothing in your program executed, which is why a print() you added to debug it prints nothing. Look at the output of the panels below and notice what is missing: there is no "Traceback (most recent call last)" line on any of them, because there is no call stack yet. That absence is the quickest way to tell a file that would not compile from one that ran and broke.

SyntaxError A missing colon

A missing colon

raises SyntaxError
def greet(name)
    return "hi " + name

The traceback it really produced, when the build ran it

  File "example.py", line 1
    def greet(name)
                   ^
SyntaxError: expected ':'

What it means. Python reached the end of the def line still expecting the colon that introduces the body. On 3.10 and later the message names the exact character it wanted, which is a large improvement on the older "invalid syntax".

The fix. Add the colon: def greet(name):. The same error covers if, for, while, class and try.

IndentationError A block header with no block

A block header with no block

raises IndentationError
def greet(name):
return name

The traceback it really produced, when the build ran it

  File "example.py", line 2
    return name
    ^^^^^^
IndentationError: expected an indented block after function definition on line 1

What it means. Indentation is not style in Python, it is syntax: it is how the language knows what is inside the function. A def line promises an indented block underneath and this file did not deliver one.

The fix. Indent the body, conventionally by four spaces. If you genuinely want an empty block, write pass.

IndentationError Indentation that means nothing

Indentation that means nothing

raises IndentationError
x = 1
    y = 2

The traceback it really produced, when the build ran it

  File "example.py", line 2
    y = 2
IndentationError: unexpected indent

What it means. The opposite mistake: an indented line with nothing to be indented under. Python only accepts a deeper indent immediately after a line ending in a colon.

The fix. Line the statement up with the one above it. This often appears after deleting an if and forgetting its body.

TabError Tabs and spaces mixed

Tabs and spaces mixed

raises TabError
def f():
    if True:
	pass

The traceback it really produced, when the build ran it

  File "example.py", line 3
    	pass
    ^
TabError: inconsistent use of tabs and spaces in indentation

What it means. One line here is indented with spaces and the next with a tab. They can look identical on screen while being different bytes, so Python refuses to guess which nesting level you meant.

The fix. Use spaces everywhere — four per level is the convention in PEP 8. Set your editor to insert spaces when you press Tab, and turn on whitespace rendering once so you can see the difference.

Names

Python looked for a name and either could not find it, or found it before it had a value.

NameError A name that does not exist

A name that does not exist

raises NameError
total = 10
print(totl)

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    print(totl)
          ^^^^
NameError: name 'totl' is not defined. Did you mean: 'total'?

What it means. Nothing called totl exists in any scope Python can see from here. Usually a typo, sometimes a name defined in another function, sometimes one you meant to import.

The fix. Read the suggestion. Since 3.12 the interpreter offers the closest name it does know, and here it is exactly right.

UnboundLocalError A local used before it was assigned

A local used before it was assigned

raises UnboundLocalError
count = 0

def bump():
    count = count + 1

bump()

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 6, in <module>
    bump()
    ~~~~^^
  File "example.py", line 4, in bump
    count = count + 1
            ^^^^^
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

What it means. This one is genuinely surprising, and it is about scope rather than typing. Because bump assigns to count anywhere in its body, Python treats count as local to bump for the whole function — so the count on the right-hand side is the local one, which has no value yet. The global count = 0 is never consulted.

The fix. If you meant to read the outer value, do not assign to the name: return count + 1. If you really meant to rebind the global, declare global count — though a parameter and a return value is almost always the better design. Lesson 17 works through this.

Types

The value is the wrong kind of thing for what you asked. Python will not quietly coerce between types the way JavaScript does, which is why these surface early rather than as bad data later.

TypeError Adding a string to a number

Adding a string to a number

raises TypeError
age = 30
print("age: " + age)

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    print("age: " + age)
          ~~~~~~~~^~~~~
TypeError: can only concatenate str (not "int") to str

What it means. Python will not decide for you whether + here means arithmetic or concatenation, so it refuses. This is the error JavaScript does not give you: there, "age: " + 30 quietly produces a string and a genuine bug survives to production.

The fix. Convert explicitly with str(age), or better, use an f-string: f"age: {age}".

TypeError Calling something that is not a function

Calling something that is not a function

raises TypeError
total = 5
total()

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    total()
    ~~~~~^^
TypeError: 'int' object is not callable

What it means. The parentheses mean "call this", and an integer is not callable.

The fix. Usually a name collision — a variable has shadowed a function of the same name. Check what the name is actually bound to at that point.

TypeError A missing argument

A missing argument

raises TypeError
def area(w, h):
    return w * h

area(3)

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 4, in <module>
    area(3)
    ~~~~^^^
TypeError: area() missing 1 required positional argument: 'h'

What it means. The function requires two positional arguments and got one. The message names the one it did not receive.

The fix. Pass it, or give the parameter a default in the definition. Lesson 15 covers why that default must not be a mutable value.

TypeError An unhashable dict key

An unhashable dict key

raises TypeError
seen = {}
seen[[1, 2]] = "x"

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    seen[[1, 2]] = "x"
    ~~~~^^^^^^^^
TypeError: cannot use 'list' as a dict key (unhashable type: 'list')

What it means. A dict key has to be hashable, and hashability requires immutability — if a key could change after insertion, the dict could never find it again. Lists are mutable, so they are unhashable by design.

The fix. Use a tuple: seen[(1, 2)] = "x". Tuples are immutable and therefore hashable, which is one of the main reasons the type exists. Lesson 7 covers this.

TypeError Indexing something that does not support it

Indexing something that does not support it

raises TypeError
n = 42
print(n[0])

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    print(n[0])
          ~^^^
TypeError: 'int' object is not subscriptable

What it means. Square brackets ask for an item by key or index, and a plain integer has no items.

The fix. Check what the value really is at that line — this often means a function returned a number where you expected a list. print(type(n)) settles it in one line.

AttributeError A method that does not exist on that type

A method that does not exist on that type

raises AttributeError
text = "hello"
text.push("x")

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    text.push("x")
    ^^^^^^^^^
AttributeError: 'str' object has no attribute 'push'

What it means. Strings have no push; that is a JavaScript array method. And strings are immutable anyway, so no method could append to one in place.

The fix. Build a new string: text + "x". For lists the method is append, and the suggestion in the message is the fastest way to find the right name when you are close.

Values

The type was right, but that particular value was not usable.

ValueError Text that is not a number

Text that is not a number

raises ValueError
print(int("twelve"))

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 1, in <module>
    print(int("twelve"))
          ~~~^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'twelve'

What it means. The argument was the right type — int() does accept a string — but that particular string does not spell a number. Type right, value wrong, which is exactly the distinction between TypeError and ValueError.

The fix. Validate before converting, or catch it: try: int(s) / except ValueError:. Never assume text from a user or a file is numeric.

ValueError Unpacking the wrong number of values

Unpacking the wrong number of values

raises ValueError
a, b = [1, 2, 3]

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 1, in <module>
    a, b = [1, 2, 3]
    ^^^^
ValueError: too many values to unpack (expected 2, got 3)

What it means. Unpacking requires the shape on the left to match the length on the right exactly. Two names, three values.

The fix. Match the count, or absorb the rest with a starred name: a, b, *rest = [1, 2, 3]. Lesson 7 covers unpacking properly.

Lookups

You asked a container for something it does not have.

IndexError An index past the end

An index past the end

raises IndexError
xs = [1, 2, 3]
print(xs[5])

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    print(xs[5])
          ~~^^^
IndexError: list index out of range

What it means. Valid indices for a list of three are 0, 1 and 2 — and -1, -2, -3 counting from the end. Five is not among them.

The fix. Check len(xs) before indexing, or iterate with for x in xs so there is no index to get wrong. An off-by-one in a range() is the usual cause.

KeyError A key that is not in the dict

A key that is not in the dict

raises KeyError
user = {"name": "Ada"}
print(user["email"])

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    print(user["email"])
          ~~~~^^^^^^^^^
KeyError: 'email'

What it means. Square brackets on a dict demand the key exist. The message is the key it could not find.

The fix. Use user.get("email") for None instead of an error, user.get("email", "") for a default, or test with if "email" in user. Lesson 8 covers when each is right.

Arithmetic

The operation is defined, but not for these numbers.

ZeroDivisionError Division by zero

Division by zero

raises ZeroDivisionError
def average(values):
    return sum(values) / len(values)

average([])

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 4, in <module>
    average([])
    ~~~~~~~^^^^
  File "example.py", line 2, in average
    return sum(values) / len(values)
           ~~~~~~~~~~~~^~~~~~~~~~~~~
ZeroDivisionError: division by zero

What it means. Written as the way this actually reaches you: not the literal 1/0 nobody writes, but an average over a list that happened to be empty. Note the two frames in the traceback — the bottom one is where it broke, the top one is the call that led there.

The fix. Guard the edge case: if not values: return 0 — or raise a clearer error of your own, which is what lesson 23 argues for.

OverflowError A float too large to represent

A float too large to represent

raises OverflowError
import math

print(math.exp(10000))

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 3, in <module>
    print(math.exp(10000))
          ~~~~~~~~^^^^^^^
OverflowError: math range error

What it means. Python integers grow without limit, but floats are IEEE-754 doubles and cap out around 1.8e308. math.exp(10000) is far past that. Worth knowing that plain 10.0 ** 10000 raises this too, while the integer 10 ** 10000 is fine.

The fix. Work in logarithms instead of raw magnitudes, which is what every library computing probabilities does for exactly this reason.

Imports

Python could not find the module, or found it and not the name you wanted from it.

ModuleNotFoundError A module that is not installed

A module that is not installed

raises ModuleNotFoundError
import reqeusts

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 1, in <module>
    import reqeusts
ModuleNotFoundError: No module named 'reqeusts'

What it means. Python searched every directory on sys.path and found nothing by that name. Either it is misspelled (as here), or it is a third-party package that is not installed in the environment you are running.

The fix. Fix the spelling, or install it into the right environment. Lesson 21 covers why "the right environment" is the hard half of that sentence.

ImportError A name that is not in that module

A name that is not in that module

raises ImportError
from math import sqrtt

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 1, in <module>
    from math import sqrtt
ImportError: cannot import name 'sqrtt' from 'math' (<python>/lib/python3.14/lib-dynload/math.cpython-314-darwin.so). Did you mean: 'sqrt'?

What it means. The module was found; the name inside it was not. The path in the message is the file Python actually loaded, which is useful when you suspect you are importing a different version of something than you think.

The fix. Read the suggestion, or run import math; print(dir(math)) to list what is really there.

Files and data

The outside world did not match what the code assumed: a path that is not there, bytes that are not the text you expected, JSON that is not JSON.

FileNotFoundError A path that is not there

A path that is not there

raises FileNotFoundError
open("no-such-file.txt")

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 1, in <module>
    open("no-such-file.txt")
    ~~~~^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'no-such-file.txt'

What it means. Relative paths are resolved against the working directory of the process, not the folder the script lives in. That difference is the cause of most of these: the file is where you think, and the program is looking somewhere else.

The fix. Print Path.cwd() to see where it is really looking, and build paths from Path(__file__).parent when you mean "next to this script". Lesson 25 covers it.

IsADirectoryError A directory where a file was expected

A directory where a file was expected

raises IsADirectoryError
open(".")

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 1, in <module>
    open(".")
    ~~~~^^^^^
IsADirectoryError: [Errno 21] Is a directory: '.'

What it means. The path exists, so this is not FileNotFoundError, but it is a directory and cannot be read as a stream of bytes.

The fix. Check with Path(p).is_file() before opening, and use Path(p).iterdir() when you want to list a directory instead.

UnicodeDecodeError Bytes that are not the text you assumed

Bytes that are not the text you assumed

raises UnicodeDecodeError
data = b"\xff\xfe"
print(data.decode("utf-8"))

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    print(data.decode("utf-8"))
          ~~~~~~~~~~~^^^^^^^^^
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte

What it means. Bytes are not text until something decodes them, and 0xff is not a legal first byte of a UTF-8 sequence. This is the error behind most "it worked on my machine" file bugs, because the default encoding has historically differed between platforms.

The fix. Name the encoding explicitly every time you open a text file — encoding="utf-8" — rather than relying on a default. Lesson 25 is built around this.

JSONDecodeError JSON that is not valid

JSON that is not valid

raises JSONDecodeError
import json

json.loads("{not valid}")

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 3, in <module>
    json.loads("{not valid}")
    ~~~~~~~~~~^^^^^^^^^^^^^^^
  File "<python>/lib/python3.14/json/__init__.py", line 352, in loads
    return _default_decoder.decode(s)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^
  File "<python>/lib/python3.14/json/decoder.py", line 345, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "<python>/lib/python3.14/json/decoder.py", line 361, in raw_decode
    obj, end = self.scan_once(s, idx)
               ~~~~~~~~~~~~~~^^^^^^^^
json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

What it means. The message gives a line, a column and a character offset, which is the fastest way into a large document. Note the type: json.decoder.JSONDecodeError, a subclass of ValueError — so except ValueError catches it too.

The fix. Never assume a response or a file is valid JSON. Catch it, and log the first hundred characters of what you actually received. Lesson 26 covers this.

Protocols and control flow

Raised by a mechanism working as designed, not necessarily by a mistake.

StopIteration An exhausted iterator

An exhausted iterator

raises StopIteration
it = iter([1])
next(it)
next(it)

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 3, in <module>
    next(it)
    ~~~~^^^^
StopIteration

What it means. Not a bug — this is the iteration protocol working. It is how an iterator says "nothing left", and it is exactly what a for loop catches invisibly on every loop it ever finishes. You only see it when calling next() yourself.

The fix. Give next a default so there is nothing to raise: next(it, None). Lesson 33 builds a for loop out of this protocol by hand.

RecursionError Recursion with no base case

Recursion with no base case

raises RecursionError
def countdown(n):
    return countdown(n - 1)

countdown(5)

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 4, in <module>
    countdown(5)
    ~~~~~~~~~^^^
  File "example.py", line 2, in countdown
    return countdown(n - 1)
  File "example.py", line 2, in countdown
    return countdown(n - 1)
  File "example.py", line 2, in countdown
    return countdown(n - 1)
  [Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceeded

What it means. Every call needs a case that returns without recursing, and this one has none, so it descends until CPython hits its own limit (around a thousand frames by default). Notice how the traceback collapses the repetition into a single "[Previous line repeated ...]" line rather than printing a thousand identical frames.

The fix. Add the base case: if n <= 0: return 0. If the recursion is genuinely deep and correct, rewrite it as a loop — raising the limit trades this error for a crash.

AssertionError An assertion that did not hold

An assertion that did not hold

raises AssertionError
total = 3
assert total == 4, "total should be four"

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    assert total == 4, "total should be four"
           ^^^^^^^^^^
AssertionError: total should be four

What it means. An assert that fails raises this, carrying whatever message you gave it. The important caveat: assertions are removed entirely when Python runs with -O, so they are a development check, never a validation you rely on.

The fix. Keep assert for internal invariants and tests. For anything that must hold in production — user input, API responses — raise a real exception instead.

NotImplementedError A method a subclass was meant to override

A method a subclass was meant to override

raises NotImplementedError
class Shape:
    def area(self):
        raise NotImplementedError("subclasses must implement area()")

Shape().area()

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 5, in <module>
    Shape().area()
    ~~~~~~~~~~~~^^
  File "example.py", line 3, in area
    raise NotImplementedError("subclasses must implement area()")
NotImplementedError: subclasses must implement area()

What it means. Raised on purpose, as documentation that runs: this base class is declaring that area is a subclass’s job. Not to be confused with NotImplemented, which is a value returned by comparison dunders, not an exception.

The fix. Nothing to fix here — it is behaving as designed. For a stricter version that fails at instantiation rather than at call time, use abc.ABC with @abstractmethod. Lesson 30 covers both.

RuntimeError A dict resized while being iterated

A dict resized while being iterated

raises RuntimeError
prices = {"book": 12, "pen": 1}

for name in prices:
    prices[name + " (sale)"] = 0

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 3, in <module>
    for name in prices:
                ^^^^^^
RuntimeError: dictionary changed size during iteration

What it means. A dict refuses to be resized mid-iteration, because its internal layout may move and the loop would then be walking freed memory. Note that a list does not raise here: it silently skips elements instead, which is worse. Lesson 45 shows that happening.

The fix. Iterate over a snapshot — for name in list(prices): — or build a new dict and replace the old one.

Concurrency

Threads, processes and coroutines each have a rule you can only break once. Two of these have unusually long tracebacks because the failure happens inside the standard library, several frames below your code — reading them from the bottom up is the only practical way.

RuntimeError A thread started twice

A thread started twice

raises RuntimeError
import threading

worker = threading.Thread(target=lambda: None)
worker.start()
worker.join()
worker.start()

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 6, in <module>
    worker.start()
    ~~~~~~~~~~~~^^
  File "<python>/lib/python3.14/threading.py", line 989, in start
    raise RuntimeError("threads can only be started once")
RuntimeError: threads can only be started once

What it means. A Thread object is a one-shot handle on an operating system thread, not a reusable job description. Once it has run, that object is spent — even after join() has confirmed it finished.

The fix. Build a new Thread per run, or better, use concurrent.futures.ThreadPoolExecutor and submit the work again. Lesson 41.

SyntaxError await outside a coroutine

await outside a coroutine

raises SyntaxError
def load():
    await fetch()

The traceback it really produced, when the build ran it

  File "example.py", line 2
    await fetch()
    ^^^^^^^^^^^^^
SyntaxError: 'await' outside async function

What it means. await is only meaningful inside an async def, so this is caught while the file is being compiled — which is why there is no traceback header above it. The commonest cause is adding an await to a function and forgetting to make it async.

The fix. Write async def load():. Every caller then has to await load() in turn, which is the change that tends to propagate upwards through a codebase.

RuntimeError asyncio.run inside a running loop

asyncio.run inside a running loop

raises RuntimeError
import asyncio

async def inner():
    return 1

async def outer():
    return asyncio.run(inner())

asyncio.run(outer())

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 9, in <module>
    asyncio.run(outer())
    ~~~~~~~~~~~^^^^^^^^^
  File "<python>/lib/python3.14/asyncio/runners.py", line 205, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "<python>/lib/python3.14/asyncio/runners.py", line 128, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "<python>/lib/python3.14/asyncio/base_events.py", line 720, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "example.py", line 7, in outer
    return asyncio.run(inner())
           ~~~~~~~~~~~^^^^^^^^^
  File "<python>/lib/python3.14/asyncio/runners.py", line 201, in run
    raise RuntimeError(
        "asyncio.run() cannot be called from a running event loop")
RuntimeError: asyncio.run() cannot be called from a running event loop
<sys>:0: RuntimeWarning: coroutine 'inner' was never awaited

What it means. asyncio.run creates an event loop, runs one coroutine and closes the loop. There can only be one loop running on a thread, so calling it from inside a coroutine is a contradiction. Read the last line of the traceback, not the first: the frames in between are the loop machinery.

The fix. Inside a coroutine, await instead: return await inner(). Keep exactly one asyncio.run, at the top of the program.

TimeoutError A coroutine that ran out of time

A coroutine that ran out of time

raises TimeoutError
import asyncio

async def slow():
    await asyncio.sleep(10)

async def main():
    async with asyncio.timeout(0.05):
        await slow()

asyncio.run(main())

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 8, in main
    await slow()
  File "example.py", line 4, in slow
    await asyncio.sleep(10)
  File "<python>/lib/python3.14/asyncio/tasks.py", line 705, in sleep
    return await future
           ^^^^^^^^^^^^
asyncio.exceptions.CancelledError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "example.py", line 10, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "<python>/lib/python3.14/asyncio/runners.py", line 205, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "<python>/lib/python3.14/asyncio/runners.py", line 128, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "<python>/lib/python3.14/asyncio/base_events.py", line 720, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "example.py", line 7, in main
    async with asyncio.timeout(0.05):
               ~~~~~~~~~~~~~~~^^^^^^
  File "<python>/lib/python3.14/asyncio/timeouts.py", line 115, in __aexit__
    raise TimeoutError from exc_val
TimeoutError

What it means. The asyncio.timeout block cancelled what it was waiting on and then raised. Since 3.11 this is the builtin TimeoutError, not asyncio.TimeoutError — they are the same object now, and older code catching the asyncio one still works.

The fix. Catch it: except TimeoutError:. Anything inside the block receives CancelledError first, so use try/finally there to release resources.

PicklingError A lambda sent to a process pool

A lambda sent to a process pool

raises PicklingError
import multiprocessing as mp

if __name__ == "__main__":
    with mp.Pool(2) as pool:
        pool.map(lambda n: n * 2, [1, 2, 3])

The traceback it really produced, when the build ran it

AttributeError: module '__main__' has no attribute '<lambda>'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "example.py", line 5, in <module>
    pool.map(lambda n: n * 2, [1, 2, 3])
    ~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<python>/lib/python3.14/multiprocessing/pool.py", line 367, in map
    return self._map_async(func, iterable, mapstar, chunksize).get()
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "<python>/lib/python3.14/multiprocessing/pool.py", line 774, in get
    raise self._value
  File "<python>/lib/python3.14/multiprocessing/pool.py", line 540, in _handle_tasks
    put(task)
    ~~~^^^^^^
  File "<python>/lib/python3.14/multiprocessing/connection.py", line 216, in send
    self._send_bytes(_ForkingPickler.dumps(obj))
                     ~~~~~~~~~~~~~~~~~~~~~^^^^^
  File "<python>/lib/python3.14/multiprocessing/reduction.py", line 51, in dumps
    cls(buf, protocol).dump(obj)
    ~~~~~~~~~~~~~~~~~~~~~~~^^^^^
_pickle.PicklingError: Can't pickle <function <lambda> at 0x10b8a1220>: it's not found as __main__.<lambda>
when serializing tuple item 0
when serializing tuple item 0
when serializing tuple item 3

What it means. Arguments to a worker process are serialised with pickle, and pickling a function stores only its name so the child can look it up again. A lambda has no name to look up. The same applies to a function defined inside another function, and to anything holding an open file or a socket.

The fix. Use a module-level function, or functools.partial of one. Lesson 42 runs both the failure and the fix.

The deep end

Errors from the machinery under classes. You will not meet these while learning the language, and you will meet them the first time you reach for one of its extension points.

AttributeError An attribute a __slots__ class did not declare

An attribute a __slots__ class did not declare

raises AttributeError
class Point:
    __slots__ = ("x", "y")

    def __init__(self, x, y):
        self.x, self.y = x, y

p = Point(1, 2)
p.z = 3

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 8, in <module>
    p.z = 3
    ^^^
AttributeError: 'Point' object has no attribute 'z' and no __dict__ for setting new attributes

What it means. __slots__ replaces the per-instance dictionary with a fixed set of slots, so there is nowhere to put an attribute that was not declared. The message says so explicitly — "and no __dict__ for setting new attributes" — which distinguishes it from an ordinary typo.

The fix. Add the name to __slots__, or drop __slots__ if the class needs to be open. Lesson 45 measures what it actually saves before you pay this price.

TypeError Two metaclasses in one class

Two metaclasses in one class

raises TypeError
class Serialisable(type): pass
class Observable(type): pass

class Model(metaclass=Serialisable): pass
class Widget(metaclass=Observable): pass

class Form(Model, Widget): pass

The traceback it really produced, when the build ran it

Traceback (most recent call last):
  File "example.py", line 7, in <module>
    class Form(Model, Widget): pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

What it means. A class has exactly one metaclass, and it must be a subclass of every base’s metaclass. Two unrelated ones cannot be reconciled, so this is also the reason two libraries that each use a metaclass cannot be mixed.

The fix. Write a third metaclass inheriting from both, if you control them. Better: do not use one — __init_subclass__ and class decorators compose, and lesson 48 shows the same jobs done with them.

How to read any of them. Start at the bottom: the last line is the exception type and the message, and that is the actual problem. Then read the frame directly above it, which is where it happened. Only then walk further up, which tells you how the program got there. Beginners tend to read a traceback top-down and give up in library code; the useful information is at the bottom, and since 3.11 the ~~~^^^ markers point at the precise sub-expression that failed rather than just the line.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.