Why it matters
Many confusing Python bugs have the same shape: "I changed one variable and a different one changed too", or "my program crashed with an error about recursion". Both are memory questions. Interviewers often open with "what happens in memory when you assign one list to another name?" because the answer shows whether you know what a variable really holds. This lesson gives you the picture that the next three lessons build on.
You do not need any memory knowledge to start. By the end of this lesson you will be able to draw the memory of a small program on paper and predict what it prints.
Start with the first principle: names are labels on objects
In Python, a variable is a name. The line count = 5 does not put 5 into a box called count. It creates an object for the number 5 and makes the name count point at it. You can picture each name as a sticky note stuck to an object. A second name can be stuck to the same object, and a name can be moved to a different object at any time.
Every value in Python is an object: numbers, text, lists, functions and your own classes. Every name is a reference to one of those objects. A reference is the way a name finds its object, similar to a street address written on a slip of paper.
This means the question "is it a value or a reference?" that matters in some other languages is not the right question in Python. The question that matters is this: can the object be changed in place?
- An
int, afloat, astr(text) and atuplecannot be changed after they are made. These are called immutable objects. When you writeb = 10, the namebmoves to a different object. Nothing about the old object changes. - A
list, adict(a table of key-value pairs), asetand an object of your own class can be changed in place. These are called mutable objects. When two names point at the same mutable object, a change made through one name is visible through the other.
Most memory puzzles in Python come down to this one point. Once you can tell whether an object is mutable, you can predict what a program prints.
If you know Java: Python has no primitive types. The number 5 is an object, and every name is a reference. The Java rule "a primitive copies the value, a reference copies the address" does not apply directly. The Python rule is "assignment never copies an object, it only attaches a name to it".
The two areas: the call stack and the heap
When a Python program runs, the interpreter uses two main areas of memory. The interpreter is the program that runs your Python code. The usual one is CPython, the version you get from python.org. The details below describe CPython. Other implementations may arrange memory differently, but the ideas here hold for all of them.
The call stack holds one frame for each function call that is running right now. A function is a named block of code, such as main or square. A frame holds the function's parameters (the values passed in) and its local names. A local name is a name assigned inside a function, and it exists only while that function runs.
The call stack works like a stack of plates in a canteen. When a function starts, the interpreter puts a new plate on top. When the function returns, its plate is taken off, and the names written on that plate go with it.
Plates are added and removed only at the top. This rule makes calls fast, because the interpreter only moves one marker up or down. It also means the stack has a limit, because every running function uses one plate. Python sets that limit to 1000 calls by default, as the recursion section shows.
The heap holds objects. The heap is the general pool of memory where objects live. Lists, text, numbers and instances of your classes all sit there. An object stays in memory while at least one reference points to it. When the last reference goes away, the memory can be reused. Lesson 4 explains how that happens.
The name "stack" also has a second meaning. In an algorithm, a stack is a data structure, a last-in-first-out list that you will meet in the stacks lesson. The call stack uses the same plate-on-top idea, but it is not the same thing as the data structure. Keep them apart in your head.
What a frame holds
Each function call gets its own frame. The frame is created when the call starts and removed when the call ends. A frame has room for:
- the parameters, which are names bound to the objects passed in,
- the local names assigned in the function body,
- a note of where to continue when the function returns.
The last item matters. When a function finishes, the interpreter needs to know where to return in the caller. That is why frames pile up in order: each caller waits underneath its callee.
Here is a small program with two functions. Read it first, then trace it on paper before you run it.
Type nothing into the Input box for this program. It does not read input.
total = 25At the moment square computes its value for a, the call stack has three frames. The top frame is square, with n pointing at 3. Under it is sum_of_squares, with a and b. At the bottom is main, with x and total. The frames under the top one are waiting for it to return. Only the top frame is running.
You can step through the same calls with a debugger or by adding print lines. Watch each frame appear and disappear as the calls go down and come back up.
After you trace it, watch for two things. First, the newest frame is always on top, and it is the only one that is running. Second, a frame disappears completely when its function returns, so its local names are gone. The result that a function returns survives only because the caller stores it in its own name.
Assignment attaches a name to an object
Python has one assignment rule, and it applies to every type. b = a makes b point at the same object that a points at. It never copies the object. What happens next depends on whether the object is mutable.
Here is the first example. Run it.
Type nothing into the Input box for this program.
5
99Read the first half step by step. b = a makes b point at the object 5, the same object a points at. Then b = 10 moves b to a different object, the number 10. The object 5 does not change, because integers cannot change. So a still points at 5, and the program prints 5.
Read the second half. q = p does not copy the list. It makes q point at the same list that p points at. Now p and q are two sticky notes on one list. q[0] = 99 changes that list in place, and p[0] shows the change.
The rule to remember is short. Assignment never copies an object. For an immutable object, a change to one name moves that name to a new object, so nothing else is affected. For a mutable object, a change through one name is visible through every name that points at that object. Both results are correct. Mixing them up causes most of the bugs in this module.
Objects live in the heap
Every object is created in the heap. Look at this line:
arr = [0, 0, 0]It does three things. The interpreter creates a new list object in the heap, with room for three references. It stores a reference to the integer 0 in each of the three slots. Then it makes the name arr point at the list. The name arr lives in the current frame, but the list does not.
Text works the same way. In name = "Asha", the name name points at a string object in the heap. Python can share some objects between names without you asking. For example, small integers and many short strings are reused, so two names may point at the same object even when you did not write b = a. You can check whether two names point at the same object with is, which compares object identity. For this lesson the rule is simple: if a name points at an object, the object is in the heap, and the name is a reference that lives in the frame where the name is assigned.
Worked example 1: trace a function call step by step
Go back to the sum_of_squares program. This time we trace it one event at a time. Use the frame description above as your guide.
Step 1. main starts. The interpreter creates a frame for main. In it, x points at 3. The name total is not assigned yet.
Step 2. main calls sum_of_squares(x, 4). The interpreter binds the parameters a and b to the objects 3 and 4. It creates a new frame on top of main's frame, holding a and b.
Step 3. sum_of_squares calls square(a). That means square(3). A third frame goes on top, with n pointing at 3.
Step 4. square computes r = 9 and returns 9. Its frame is removed. The value 9 is handed back to the caller.
Step 5. Back in sum_of_squares, the returned 9 is stored in sa. The next line calls square(b), which is square(4). A new frame appears with n pointing at 4. It computes r = 16, returns 16, and disappears. The value 16 is stored in sb.
Step 6. sum_of_squares computes sa + sb, which is 25, and returns it. Its frame is removed.
Step 7. main stores 25 in total, prints the line, and finishes.
The output is:
total = 25What did you learn from this trace? The frame for square was created and removed twice, and each time it held different values of n and r. The two calls did not share their names, even though both use the names n and r in the code. Each call has its own frame. This is why a recursive function can call itself without messing up its own names.
Worked example 2: attributes live inside objects
An object of your own class holds data in attributes. An attribute is a name that belongs to an object, written as obj.name. Attributes live inside the object in the heap, even when the object was created inside a function.
Here is a small class and a program that uses it. The method __init__ (a function that belongs to a class) runs when an object is created. It sets the attributes.
Type nothing into the Input box for this program.
Asha 20
FalseThe Student object sits in the heap. It has two attributes: age, which points at the integer 20, and name, which points at the string "Asha". The names s and t live in the frame of the module (the top level of the file), and both point at the same Student.
Line by line: t.age = 20 changes the attribute inside the shared object. Because s points at that same object, s.age now shows 20. Then s = None removes the sticky note from s. The object is still there, because t still points at it. So t is None is False.
The lesson here is that an attribute can point at another object. An object can hold other objects, and each link in the chain is a reference.
Worked example 3: a list of objects holds references
Lists make the difference between a value and a reference easy to see. A list of integers holds the integers. A list of objects holds references, and the slots can be empty. In Python, the empty slot is the object None.
The next program creates a list with two slots. We fill one slot and leave the other empty.
Type nothing into the Input box for this program.
Ravi 18
roster[1] is None
roster has 2 slotsTwo details matter here.
First, [None] * 2 creates a list with two slots, but it does not create any Student objects. Each slot is a reference, and each starts as None, which means "no object". The second slot is still None when we print it.
Second, make_student creates a Student and returns it. When the function ends, the local name s disappears with its frame. The object does not disappear, because the returned reference was stored in kept at module level. The object lives on in the heap for as long as some reference reaches it. This is the most important fact about the heap: an object outlives the function that made it, if someone still holds a reference.
Worked example 4: the same name, a different object
A name is only a label. Assigning a new object to a name changes which object the label points at. It does not change the old object.
a = Student("Ravi", 18) # a points at object 1
a = Student("Meena", 20) # a points at object 2; object 1 has no name nowAfter the second line, object 1 is still in memory, but no name points at it. Nothing can reach it any more, so it is garbage. Lesson 4 explains when and how that memory is freed.
The next program makes the cleanup visible. The method __del__ runs when an object is about to be freed. This is a CPython behaviour. Other Python implementations may free the object later, or not print at all.
Type nothing into the Input box for this program.
freed object 1
after reassignment
freed object 2The first object is freed the moment n1 points at the second one, because nothing else pointed at it. The second object is still referenced by n1, so it is freed when the program ends. The order of the last line is the same on every CPython run.
This pattern appears all the time in code that loops. A loop variable like s takes a new value on each pass. The objects from earlier passes may still be alive if you stored them in a list, or they may be garbage if you did not.
Worked example 5: objects created inside a loop
A loop body runs again and again, and every pass can create new objects. This is where many beginners lose track of memory, so trace it carefully. The next program creates one Student per pass.
Type nothing into the Input box for this program.
Student 1 is 19
Student 2 is 20
Student 3 is 21Trace it on paper. The loop name i and the name s live in the module's frame, and they are reused on every pass. There is only one i and only one s. What changes is the object that s points at.
On pass 1, i points at 1, the runtime creates object A, and s points at A. The line prints Student 1 is 19.
On pass 2, i moves to 2, the interpreter creates object B, and s is moved to B. Object A is still in memory, but no name points at it. It is now garbage.
On pass 3, the same thing happens with object C. When the loop ends, s points at C, and A and B are unreachable.
The lesson is that a name is a label that can be moved, while an object exists until nothing refers to it. In a loop that runs a million times, the program creates a million objects, and most of them are garbage almost as soon as they appear. The garbage collector deals with them, but creating them is not free. Lesson 4 returns to this point.
What a recursion limit looks like
The call stack has a limit. CPython stops a program when the call stack gets too deep, and the limit is set by sys.getrecursionlimit(). The default is 1000. Each function call uses one frame, so a function that calls itself without end reaches the limit. When that happens, Python raises a RecursionError. An exception is Python's name for an error that can be caught with try and except.
Here is a function with no end. We will not run it as-is in the lesson, because the result is a crash. The next program catches the error and prints a message, so you can see that the error really happens.
Type nothing into the Input box for this program.
limit: 1000
recursion error caught: maximum recursion depth exceeded
depth was positive: TrueThe try and except lines say "run this, and if this error happens, do that instead". The program prints the message and keeps going. The word global depth tells Python that depth refers to the name at module level, not a new local name. In real code, catching a recursion error is rare. The usual fix is to repair the function so that it stops.
Every recursive function needs a base case, which is a condition that ends the calls. Here is the fixed version of the same idea:
def count_down(n):
if n == 0:
return 0 # base case: stop here, no new frame
return count_down(n - 1) # each call moves closer to the base caseCalling count_down(3) makes four frames, for n equal to 3, 2, 1 and 0, and then returns 0. The recursion lesson in this course covers this pattern in detail.
The limit is a safety setting, not a fact about memory. You can raise it with sys.setrecursionlimit(5000), but a very high limit can crash the interpreter itself, because the C stack underneath is also limited. The better fix is almost always a loop or a base case.
What goes wrong in each area
Each area has its own failures. Learning the names of the errors helps you guess where the problem is.
| Error message | Area involved | Typical cause |
|---|---|---|
RecursionError: maximum recursion depth exceeded | Call stack | A function calls itself with no base case, or the recursion is deeper than expected |
MemoryError | Heap | Objects are kept alive in a list or a module-level name, and the process runs out of memory |
AttributeError: 'NoneType' object has no attribute ... | A name that points at None | The code reads an attribute or calls a method through a name that points at nothing |
The table is a starting point. A MemoryError is raised when the process cannot get more memory. The heap error is about how much data is alive, not about how deep the calls are. The recursion error is about how deep the calls are, not about how big the data is.
Here is the shape of code that causes a MemoryError. We do not run it, because it can freeze a shared machine. The point is to recognise the pattern.
keep = []
i = 0
while True:
keep.append(bytearray(1024 * 1024)) # each item is about 1 MB; the list keeps them all
i += 1The list keep holds a reference to every block, so none of them can be freed. The process grows until the operating system refuses to give it more memory.
Common mistakes
Saying that objects are on the call stack. This is a common error. The name that points at an object is in a frame. The object it points at is in the heap. In an interview, say both parts.
Believing b = a copies a list. For a list, b = a makes b point at the same list. To get an independent copy, write b = a.copy() or b = a[:]. Both make a new list with the same items. If the list contains other lists, the copy is shallow. The copy module covers that case in the variations section.
Thinking a local name keeps an object alive after the function ends. The name disappears with its frame. The object stays only if another reference still reaches it.
Using a name before it has a value. Python checks this when the code runs, not before. The error depends on where the name is used. At module level, the message is a NameError:
Traceback (most recent call last):
File "/home/you/example.py", line 1, in <module>
print(total)
^^^^^
NameError: name 'total' is not definedInside a function, the same mistake gives an UnboundLocalError. Python sees that total is assigned later in the function, so it treats the name as local from the start:
Traceback (most recent call last):
File "/home/you/example.py", line 5, in <module>
show()
~~~~^^
File "/home/you/example.py", line 2, in show
print(total)
^^^^^
UnboundLocalError: cannot access local variable 'total' where it is not associated with a valueThe file path and line numbers depend on your machine. The fix is to give total a value before the first use, such as total = 0.
Confusing None with an empty object. If a list has None slots, reading an attribute through one of them raises an error. The message names the object that was None (your file path and line number will differ):
Traceback (most recent call last):
File "/home/you/example.py", line 5, in <module>
print(roster[1].name)
^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'name'Read the message from left to right. 'NoneType' object tells you the value was None. has no attribute 'name' tells you the operation that failed. The caret marks the expression. You now know that the line before must put an object into roster[1], or the code must check for None first.
The repeated-list trap. Writing [[0] * 3] * 2 looks like it makes a grid of two rows. It does not. The outer * 2 puts the same inner list in both slots, so changing one row changes both:
Type nothing into the Input box for this program.
[[0, 7, 0], [0, 7, 0]]
[[0, 7, 0], [0, 0, 0]]The first grid has one inner list, referenced twice. The second grid makes a new inner list on each pass of the comprehension, so the rows are separate.
Confusing the two meanings of stack. A question about "a stack" in an algorithm is about the data structure, not the call stack. Make sure you know which one the question means.
Reading a traceback
When an exception is not caught, Python prints a traceback. It lists the frames that were on the call stack when the error happened. Python prints it in the opposite order from many other languages. The oldest frame, the one that started the program, is at the top. The newest frame, where the error happened, is at the bottom. Read the traceback from the bottom up, starting with the last line.
Here is the traceback from a function that calls itself without an end:
def forever(n):
return forever(n + 1)
forever(0)Running it prints this (your file path, line numbers and repeat count will differ):
Traceback (most recent call last):
File "/home/you/recurse.py", line 4, in <module>
forever(0)
~~~~~~~^^^
File "/home/you/recurse.py", line 2, in forever
return forever(n + 1)
File "/home/you/recurse.py", line 2, in forever
return forever(n + 1)
File "/home/you/recurse.py", line 2, in forever
return forever(n + 1)
[Previous line repeated 996 more times]
RecursionError: maximum recursion depth exceededRead it in four steps. The last line gives the error type, RecursionError, and its message. The line above it, return forever(n + 1), is the code that was running when the limit was reached. Python shows the same line many times because it repeated the call. The [Previous line repeated 996 more times] text is Python's way of saying the repetition was too long to print. When the same line repeats many times, you have found a recursion with no end. Your next step is to open that line and ask what stops the calls.
Now the AttributeError from the common mistakes section:
Traceback (most recent call last):
File "/home/you/example.py", line 5, in <module>
print(roster[1].name)
^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'name'There is only one frame, <module>, so the error happened directly at module level, on line 5. The message after the colon is the most useful part. It says the attribute name was read from a None value. You know which line to check.
A short habit makes tracebacks easy to read. Start at the last line, read the message, then go up to the line just above it. Check the values that line uses. Do not start from the top of a long traceback, because the top shows the setup, not the cause.
Debugging walkthrough: "my other list changed"
Here is a bug report that comes up constantly. A student writes this code and is confused by the output.
scores = [50, 60]
backup = scores
backup[0] = 0
print(scores)The program prints [0, 60], but the student expected [50, 60]. Follow the same four questions every time.
Question 1: what kind of object is it? scores points at a list. A list is mutable, so it can be changed in place.
Question 2: what does the assignment do? backup = scores makes backup point at the same list. Nothing is copied.
Question 3: draw the sticky notes. Draw scores and backup as two labels, both pointing at one list box. The picture makes it obvious that a change through backup is a change to scores.
Question 4: what does the author want? The student wants a separate copy. The fix is to copy the list, so there are two list objects.
Type nothing into the Input box for this program.
[50, 60]
[0, 60]Now the two names point at two separate lists. Changing one does not affect the other. This is the habit to build: when a bug says "a different variable changed", find the assignment that made the two names share one object, and decide whether sharing was what you wanted.
Variations and edge cases
Class attributes. An attribute written in the class body, outside any method, belongs to the class, not to one object. It is shared by all objects of the class, and it lives as long as the class does. It is the nearest Python equivalent of a static field in other languages. Lesson 4 uses this fact to explain memory leaks.
Type nothing into the Input box for this program.
2Nested lists. A two-dimensional list such as grid = [[0] * 4 for _ in range(3)] is a list of references, where each reference leads to a list of four numbers. There are four list objects in the heap: the outer list and three inner lists. Lesson 4 uses this when it explains copying.
Copying nested data. a.copy() and a[:] make a shallow copy. The new outer list points at the same inner lists as the old one. To copy everything, use copy.deepcopy:
Type nothing into the Input box for this program.
99
1The shallow copy shows the change, because its inner list is the same object. The deep copy does not, because it has its own inner lists.
Empty strings and None. An empty string "" is an object with no characters. It is not None. Calling len("") returns 0. None has no length at all, so len(None) raises a TypeError.
Parameters versus local names. Parameters are local names that get their objects from the caller. They follow the same rules. A parameter that points at a mutable object can change the shared object, and that is the topic of lesson 3.
Cost and speed
Creating a local name is almost free, because it is a slot in a frame that already exists. Creating a frame and removing it is fast, but a function call still costs more than an assignment, because the interpreter does extra work. Creating an object costs more, because the interpreter must find free memory and fill it in. Reading an attribute through a name costs a little more than reading a local name, because the interpreter looks up the attribute first.
For most programs these costs do not matter. They matter when you create millions of short-lived objects in a tight loop. A common fix is to reuse one object, or to store plain numbers in a list from the array module or a NumPy array, instead of creating one object for each number. The exact cost depends on your machine and Python version, so measure before you change code.
Interview questions on this topic
"Where do local names live in Python?" In the frame of the function that assigns them. Each name points at an object, and the frame is removed when the function returns.
"Where does a list live?" In the heap. The name that points at the list is in a frame, or it is an attribute of an object.
"What is the difference between a function's frame and an object?" The frame holds the names and the return point for one running call. The object holds data, and it lives until nothing refers to it. A frame is removed when its function returns. An object can outlive the function that made it.
"What happens when a function returns an object?" Only the reference is returned, and the caller stores it in its own name. The object stays in the heap. The function's local name is gone.
"Is a string stored on the stack or the heap?" The string is an object in the heap. A local name that points at it is in a frame. A good answer names both parts.
"If Python has a garbage collector, can there be a memory leak?" Yes. A leak happens when objects stay reachable from a live reference even though the program no longer needs them. The collector only frees what nothing reaches, so memory grows slowly. Lesson 4 has a complete example.
Follow-up you should expect: "Why does deep recursion raise an error but a large list does not?" The answer is that the call stack has a set limit on depth, and each call uses one frame. A list is an object in the heap, and it can grow until the process runs out of memory.
Real-world uses
A web server such as Gunicorn or Django runs requests in separate processes or threads. Each one has its own call stack. A parser that calls itself on every nested bracket can exceed the recursion limit when the input is unusually deep. The traceback tells you which function recursed.
Large data such as images, query results and lists of records goes in the heap. When a process runs out of memory, the operating system may stop it, or Python raises MemoryError. Engineers limit memory with tools such as container memory limits, or they process data in chunks instead of loading all of it at once.
Practice
Try each exercise on paper first. Use the sticky-note picture from this lesson: a label for each name, a box for each object, and an arrow for each reference.
- Draw the names and objects for
n = 3; s = "hi"; a = [0, 0]. Which of the three names points at a mutable object? - What does this print?
x = [1]; y = x; y[0] = 5; print(x[0]) - Write a function
forever2(n)that calls itself with no base case, then fix it by adding a base case that returns 0 whennis 0. - Trace this code, writing down the names in each frame at the moment
greturns:def f(n): t = n + 1; return g(t)anddef g(m): return m * 2, called asf(4). - After
r = [None] * 3, what doesr[1]point at? What error do you get when you writer[1].name? - Write a function
make_ones(size)that returns a new list of the given size filled with 1s. Which area holds the list, and which holds the name that the caller receives? - What does this print?
a = "x"; b = a; a = "y"; print(b) - Explain why
def reset(data): data = [0, 0, 0]does not change the caller's list. - Explain, in three sentences, why deep recursion raises
RecursionErrorwhile a very large list does not. - Write a complete program that creates a list of 3
Studentobjects, fills only the first two, and prints each slot. Predict the output before you run it.
Show the answer
-
npoints at the integer 3,spoints at the string "hi" andapoints at a list. All three names are references. Only the list is mutable, because you can change its items in place. The integer and the string cannot be changed. -
It prints
5. The assignmenty = xmakesypoint at the same list asx. Changingy[0]changes that list, andx[0]shows the change. -
One possible answer:
def forever2(n):
if n == 0:
return 0
return forever2(n - 1)The base case n == 0 stops the calls. Without the if, the function would call itself forever and reach the recursion limit.
-
f(4)runs first. Its frame hasn = 4andt = 5. It callsg(5), so a new frame forgappears withm = 5.gcomputes5 * 2, which is 10, and returns it. At the momentgreturns, the frames are: the module,f(withn = 4andt = 5) andg(withm = 5, returning 10). -
r[1]points atNone, because each slot of a new list starts asNone. Writingr[1].nameraisesAttributeError: 'NoneType' object has no attribute 'name'. -
One possible answer:
def make_ones(size):
result = [0] * size
for i in range(size):
result[i] = 1
return resultThe list itself is in the heap. The name result is in the function's frame and disappears when the function returns. The caller receives the reference and stores it in its own name. Writing return [1] * size gives the same result.
-
It prints
x. Strings cannot be changed.a = "y"movesato a different string, andbstill points at"x". -
The parameter
datastarts as a reference to the caller's list. The assignment movesdatato a new list of zeros. The caller's name still points at the original list, so the caller sees no change. Only edits to items throughdata, such asdata[0] = 0, would reach the caller's list. -
Each function call creates a frame on the call stack, and the stack has a set limit on depth. Deep recursion uses up the limit, and Python raises
RecursionError. A large list is an object in the heap, and it uses one reference on the call stack no matter how big it is. Its size does not add frames. -
One possible program, with its predicted output:
Running it prints:
slot 0 : Asha
slot 1 : Ravi
slot 2 : NoneThe prediction is that slots 0 and 1 print the names Asha and Ravi, and slot 2 prints None. The key point is that the slot holding None has no object, so you must check for it before you read an attribute.
What you now know
- The call stack holds one frame per running function, with its parameters and local names. A frame is removed when its function returns.
- The heap holds objects, including lists, strings, numbers and instances of your classes.
- Every name is a reference to an object. Assignment never copies an object. It only attaches a name to it.
- For an immutable object such as an integer or a string, changing a name moves it to a new object. For a mutable object such as a list, a change is visible through every name that points at it.
- An object lives as long as some reference reaches it, even after the function that created it returns.
RecursionErrorcomes from too many nested frames.MemoryErrorcomes from too many live objects.