Everything in Python Is an Object
In CPython (the standard Python implementation), every value you work with — an integer, a string, a function, even a class itself — is a full-blown object stored in memory. Each object carries three pieces of information:- A value — the data it holds (e.g.,
42or"hello") - A type — what the object can do (e.g.,
<class 'int'>) - An identity — a unique integer representing its memory address, returned by
id()
Mutable vs Immutable Objects
Python divides objects into two camps. Understanding which camp each type belongs to prevents an entire category of subtle bugs.- Mutable Objects
- Immutable Objects
A mutable object can be changed in-place after creation. Its memory address stays the same even when its contents change.Mutable types: Because mutations happen in-place, any variable that references the same object sees the change:
list, dict, set, and most custom class instances.Identity (is) vs Equality (==)
== checks whether two objects have the same value. is checks whether they are the same object in memory — i.e., id(a) == id(b).
== when you care about value, and is only when checking object identity (e.g., if result is None).
Reference Counting and Garbage Collection
CPython tracks how many variables point to each object using a reference count. When you assign an object to a variable, its count goes up. When the variable goes out of scope or is reassigned, the count goes down. When the count reaches zero, Python immediately frees the memory.The Global Interpreter Lock (GIL)
The GIL is a mutex inside CPython that ensures only one thread executes Python bytecode at a time. It exists to protect CPython’s reference-counting memory model from race conditions.What the GIL prevents
Multiple threads cannot corrupt an object’s reference count by incrementing or decrementing it simultaneously — the GIL serialises those operations.
What it means for you
CPU-bound tasks (e.g., number crunching) do not get faster with Python threads because only one thread runs at a time. Use
multiprocessing or libraries like NumPy (which release the GIL) instead.asyncio work well because Python releases the GIL while waiting on I/O.
Shallow Copy vs Deep Copy
When you need a copy of a collection rather than a shared reference, you must choose between shallow and deep copying.When to use shallow copy
When to use shallow copy
Use
copy.copy() (or list slicing lst[:]) when your collection contains only immutable objects like strings or numbers. A shallow copy is faster and sufficient because the elements cannot be mutated anyway.When to use deep copy
When to use deep copy
Use
copy.deepcopy() when your collection contains nested mutable objects (lists of lists, dicts of lists, etc.) and you want a fully independent copy that shares no references with the original.Practical Implications
1
Avoid mutable default arguments
Never use a mutable object as a function default argument. The same object is shared across all calls:
2
Use is None, not == None
None is a singleton — there is exactly one None object in CPython. Use is None and is not None to check for it, not == None.3
Be deliberate about shared references
When you pass a list to a function, the function receives a reference to the same object. Mutations inside the function affect the caller’s list.Pass
items[:] or copy.copy(items) if you want the function to work on an independent copy.Python 3.12 introduced a per-interpreter GIL option, and Python 3.13 adds an experimental free-threaded build (
--disable-gil). The landscape around the GIL is actively evolving, but for everyday Python the behaviour described here applies.