Anik Sikder
Technical Writing/python/python-variables-and-memory-the-deep-dive-every-beginner-should-read
article.sh

$ open article

python

Python Variables & Memory: The Deep-Dive Every Beginner Should Read

10 min readAugust 13, 2025
Python Variables, Memory References, and Garbage Collection Visualization

Hey Python enthusiasts! 👋

Have you ever wondered:

  • Why changing one list sometimes changes another?
  • Why does a is b behave differently from a == b?
  • What actually happens when you assign a variable?
  • How does Python know when to free memory?

If you've ever felt confused by variables, memory references, mutability, or garbage collection, you're definitely not alone.

Today we're diving deep into one of the most important concepts in Python:

Variables and Memory Management.

Understanding this topic will instantly make many Python "mysteries" suddenly make sense.

Ready? Let's jump in. 🚀

Variables Are Not Boxes

One of the biggest misconceptions beginners have is thinking variables are containers that hold values.

They're not.

In Python, variables are simply names bound to objects.

Consider:

code
x = 10

Many people imagine:

code
x
┌────┐
│ 10 │
└────┘

But Python actually works more like:

code
x ─────► Object(10)

The variable is merely a label pointing to an object in memory.

Think of it as a sticky note attached to an object.

The note isn't the object.

It's just a way to find it.


Memory References

Let's see this in action.

code
x = 10
y = x

Now both names point to the same object.

code
x ──► 10
y ──► 10

We can verify this using id():

code
x = 10
y = x

print(id(x))
print(id(y))

Both IDs will be identical.

Because both names reference the same object.


Rebinding vs Mutation

A crucial distinction in Python is the difference between:

  • Rebinding a name
  • Mutating an object

Rebinding

code
x = 10
x = 20

The original object wasn't changed.

Instead:

code
x ──► 10

becomes

x ──► 20

The label moved.


Mutation

code
x = [1, 2, 3]
y = x

y.append(4)

Now:

code
print(x)

Outputs:

code
[1, 2, 3, 4]

Why?

Because both names reference the same list object.

The object changed.

The names didn't.


Reference Counting

CPython uses reference counting to track memory.

Every object maintains a count of how many references point to it.

Example:

code
import sys

a = [1, 2, 3]

print(sys.getrefcount(a))

Create another reference:

code
b = a

print(sys.getrefcount(a))

The count increases.

Remove it:

code
del b

print(sys.getrefcount(a))

The count decreases.

When the count reaches zero, Python can safely free the object.


Garbage Collection

Reference counting is fast.

But it has one weakness:

Circular References

Imagine:

code
class Node:
    def __init__(self):
        self.ref = None

a = Node()
b = Node()

a.ref = b
b.ref = a

Now:

code
a → b
↑   ↓
└───┘

Even after:

code
del a
del b

The objects still reference each other.

Reference counts never reach zero.

Without help, this becomes a memory leak.


Cycle Detection

To solve this, Python includes a garbage collector.

code
import gc

gc.collect()

The collector periodically searches for unreachable cycles and removes them.

This is why CPython uses:

  1. Reference counting
  2. Cycle-detecting garbage collection

Together.


Dynamic Typing

Python is dynamically typed.

Names can point to any object.

code
x = 10
x = "hello"
x = [1, 2, 3]

Perfectly valid.

The name changes.

The objects don't.


Strong Typing

Python is also strongly typed.

It won't silently mix incompatible types.

code
"10" + 5

Produces:

code
TypeError

Unlike some languages, Python refuses to guess your intentions.


Mutable vs Immutable Objects

This distinction explains countless bugs.

Immutable Objects

Cannot be modified after creation.

Examples:

  • int
  • float
  • bool
  • str
  • tuple
  • frozenset
code
s = "hello"

s += "!"

A brand-new string is created.

The original string remains unchanged.


Mutable Objects

Can be modified in place.

Examples:

  • list
  • dict
  • set
code
nums = [1, 2]

nums.append(3)

The same list object changes.

No new object is created.


A Sneaky Tuple Example

Tuples are immutable.

But they can contain mutable objects.

code
t = ([], 42)

t[0].append("boom")

print(t)

Output:

code
(['boom'], 42)

The tuple didn't change.

The list inside it did.

Mind-blowing the first time you see it.


Function Arguments & Memory

Python uses call-by-sharing.

When you pass an argument, Python passes the object reference.

Consider:

code
def mutate(lst):
    lst.append(99)

a = [1, 2, 3]

mutate(a)

print(a)

Output:

code
[1, 2, 3, 99]

The function received a reference to the same list.


Rebinding Inside Functions

Now compare:

code
def rebind(lst):
    lst = lst + [99]

a = [1, 2, 3]

rebind(a)

print(a)

Output:

code
[1, 2, 3]

Why?

Because the function created a new list and rebound its local variable.

The original list remained untouched.


The Famous Mutable Default Argument Trap

One of Python's most famous gotchas:

code
def add_item(item, bucket=[]):
    bucket.append(item)
    return bucket

Looks innocent.

But:

code
print(add_item(1))
print(add_item(2))

Outputs:

code
[1]
[1, 2]

The default list is created once.

Not every call.


The Correct Approach

code
def add_item(item, bucket=None):
    if bucket is None:
        bucket = []

    bucket.append(item)
    return bucket

Always use this pattern for mutable defaults.


Shared References

Multiple names can point to the same mutable object.

code
a = [1, 2]

b = a

b.append(3)

print(a)

Output:

code
[1, 2, 3]

Sometimes that's useful.

Sometimes it's a bug.

Know the difference.


Shallow Copy vs Deep Copy

To avoid shared references:

code
import copy

x = [[1], [2]]

y = copy.copy(x)

z = copy.deepcopy(x)

Shallow Copy

Copies only the outer container.

Nested objects remain shared.

Deep Copy

Recursively copies everything.

No shared references remain.


Equality vs Identity

This trips up nearly every beginner.

Value Equality

code
a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)

Output:

code
True

Values are identical.


Identity

code
print(a is b)

Output:

code
False

Different objects.

Different memory locations.


Use is for None

The canonical pattern:

code
if value is None:
    ...

Not:

code
if value == None:
    ...

Identity checks are more precise.


Everything Is an Object

Functions?

Objects.

Classes?

Objects.

Modules?

Objects.

Numbers?

Objects.

code
x = 42

def greet():
    pass

print(type(x))
print(type(greet))

Everything in Python is built around objects.


Namespaces and LEGB

Names live inside namespaces.

Python resolves names using:

LEGB

  1. Local
  2. Enclosing
  3. Global
  4. Built-in

Example:

code
x = "global"

def outer():
    x = "enclosing"

    def inner():
        x = "local"
        print(x)

    inner()

outer()

Output:

code
local

Python always searches in LEGB order.


CPython Interning

CPython optimizes memory by reusing certain objects.

Small Integers

code
a = 256
b = 256

print(a is b)

Usually:

code
True

Because Python reuses small integer objects.


Strings

code
a = "hello"
b = "hello"

print(a is b)

Often:

code
True

Due to string interning.


Important Rule

Never rely on interning.

Use:

code
==

For value comparison.

Not:

code
is

Constant Folding

Python performs compile-time optimizations.

Example:

code
x = 2 * 10

Often becomes:

code
x = 20

Before execution even starts.

Similarly:

code
"a" + "b"

May become:

code
"ab"

At compile time.


Why This Matters

Understanding variables and memory helps you:

  • Debug weird bugs
  • Avoid accidental mutations
  • Write safer functions
  • Understand performance better
  • Master Python internals
  • Perform better in interviews

Many advanced Python topics become much easier once this mental model clicks.


TL;DR Quick Recap

  • Variables are names bound to objects.
  • Multiple names can point to the same object.
  • Mutable objects can change in place.
  • Immutable objects cannot.
  • CPython uses reference counting.
  • Garbage collection handles cycles.
  • == compares values.
  • is compares identities.
  • Avoid mutable default arguments.
  • Use deepcopy() carefully when needed.
  • Interning is an optimization, not a guarantee.

Final Thoughts: The Mental Model That Changes Everything 🧠

The biggest lesson here is simple:

Variables don't contain objects. They point to objects.

Once you internalize that idea, mutability, function calls, garbage collection, copying, and equality all start making sense.

It's one of those concepts that feels small at first but completely changes how you understand Python.

Master this mental model, and you'll avoid a huge percentage of the bugs that trip up new Python developers.


A Little Joke to End On 😄

Why did the Python variable break up with the object?

Because it realized it was just a reference and the relationship wasn't actually permanent.


Frequently Asked Questions

Are Python variables containers?

No.

Variables are names bound to objects in memory.

They do not store values themselves.


What is reference counting?

Reference counting is CPython's mechanism for tracking how many references point to an object.

When the count reaches zero, the object can be removed.


What is garbage collection in Python?

Python's garbage collector detects and removes circular references that reference counting alone cannot clean up.


What is the difference between mutable and immutable objects?

Mutable objects can change after creation.

Immutable objects cannot.

Examples:

Mutable:

  • list
  • dict
  • set

Immutable:

  • int
  • float
  • str
  • tuple

What is the difference between == and is?

==

Checks value equality.

code
[1, 2] == [1, 2]

Returns:

code
True

is

Checks object identity.

code
[1, 2] is [1, 2]

Returns:

code
False

Why should I avoid mutable default arguments?

Because default mutable objects are created only once and reused across function calls.

This can lead to unexpected behavior.


What is shallow copy vs deep copy?

Shallow copy duplicates only the outer container.

Deep copy recursively duplicates all nested objects.


What is interning in Python?

Interning is an optimization where Python reuses certain immutable objects such as small integers and some strings.


How are function arguments passed in Python?

Python uses call-by-sharing.

Functions receive references to objects, not copies of objects.


Why is understanding memory important?

It helps developers:

  • Avoid bugs
  • Understand mutability
  • Write efficient code
  • Debug reference issues
  • Master Python internals

Key Takeaways

  • Variables are names, not containers.
  • Objects live independently in memory.
  • Names can be rebound.
  • Mutable objects can be shared.
  • CPython uses reference counting.
  • Garbage collection handles cycles.
  • Use == for value comparison.
  • Use is for identity comparison.
  • Avoid mutable default arguments.
  • Understanding references makes Python far easier to reason about.

If you found this helpful, please share and follow for more Python deep dives!


About the Author

Anik Sikder is a Software Engineer specializing in Backend Systems, Cloud Infrastructure, DevOps, Python, Django, FastAPI, System Design, and Software Architecture.

He writes about Python internals, distributed systems, software engineering, and scalable backend development.

$ tags

pythonmemory-managementvariablesgarbage-collectionreference-countingmutabilitybackendsoftware-engineering

$ ls related_articles

status: end_of_file