This lesson connects four confusing topics into one clear mental model: objects, references, memory, and timing.
If the editor doesn’t load on Blogger: open it in a new tab (button above). Some sites send headers that block embedding.
In Python, a variable is not a “box holding a value”. It is a name (label) pointing to an object. The function id(obj) returns a unique identity number for that object during its lifetime.
x = 10
print("x =", x)
print("id(x) =", id(x))
If two names point to the same object, they will share the same id. This is why is checks identity:
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a is b) # True (same object)
print(a == c) # True (same value)
print(a is c) # False (different objects)
Many learners ask: “If I know an object’s id, can I get the variable value?” In normal Python, the answer is no, because Python does not keep a built-in reverse map like:
Also, the question becomes ambiguous because one object can have many names:
a = 100
b = 100
print("id(a) =", id(a))
print("id(b) =", id(b))
print("a is b:", a is b)
If a and b point to the same object, then “return the value of the variable” is not meaningful. Which variable name should Python return? There may be multiple.
Correct approach (if you truly need lookup): create a registry yourself.
registry = {}
obj = {"topic": "Python", "time": "7 PM"}
registry[id(obj)] = obj
print(registry[id(obj)])
Python mainly frees memory by reference counting. When an object has zero references left, it becomes eligible for cleanup. The keyword del removes a name (reference), not “the object in memory”.
x = [1, 2, 3]
print("id(x) =", id(x))
del x
# x is gone now (NameError if you print x)
But if another name points to the same object, it will remain alive:
a = [1, 2, 3]
b = a
del a
print(b) # still works, object still alive via b
Reassigning also drops a reference:
big = [i for i in range(1_000_00)]
big = None # old list may be freed if no other refs exist
Reference counting cannot free cyclic references (objects referring to each other). That’s why Python also has a garbage collector (GC) to detect cycles.
a = []
a.append(a) # a refers to itself (cycle)
print("cycle created")
You can request GC to run (rarely needed):
import gc
collected = gc.collect()
print("Collected objects:", collected)
Python objects have overhead. A list is a container that holds references to items. So sys.getsizeof() gives the size of the container object (shallow size), not the full memory of everything inside.
import sys
lst = [1, 2, 3]
print(sys.getsizeof(lst), "bytes")
To estimate total memory (deep size), you can recursively add sizes:
import sys
def deep_getsizeof(obj, seen=None):
if seen is None:
seen = set()
obj_id = id(obj)
if obj_id in seen:
return 0
seen.add(obj_id)
size = sys.getsizeof(obj)
if isinstance(obj, dict):
size += sum(deep_getsizeof(k, seen) + deep_getsizeof(v, seen)
for k, v in obj.items())
elif isinstance(obj, (list, tuple, set)):
size += sum(deep_getsizeof(i, seen) for i in obj)
return size
data = [1, 2, [3, 4], {"a": 10, "b": [11, 12]}]
print("Deep size:", deep_getsizeof(data), "bytes")
Timing is tricky because computers do background work and short statements run extremely fast. That’s why we prefer time.perf_counter() (high precision) and timeit (repeat + average).
import time
start = time.perf_counter()
x = sum(range(1_000_000))
end = time.perf_counter()
print("Time:", end - start, "seconds")
import time
t0 = time.perf_counter()
a = [i*i for i in range(200_000)]
t1 = time.perf_counter()
b = sum(a)
t2 = time.perf_counter()
c = max(a)
t3 = time.perf_counter()
print("build list:", t1 - t0)
print("sum:", t2 - t1)
print("max:", t3 - t2)
import timeit
t = timeit.timeit("sum(range(1000))", number=10000)
print("Total time:", t)
import cProfile
cProfile.run("sum(range(1_000_000))")
| Topic | Best one-liner |
|---|---|
| id() | Identity of an object during its lifetime; not a variable address you can reverse-map. |
| del | Removes a name/reference; object is freed only when no references remain. |
| gc | Collects cyclic garbage; manual gc.collect() is rarely needed. |
| Size | sys.getsizeof() is shallow; deep size requires traversal. |
| Timing | Use perf_counter for timing and timeit for benchmarks. |
a is b # same object?
a == b # same value?
id(a) # object identity
del x
x = None
import gc
gc.collect()
import sys
sys.getsizeof(obj)
import time
t0 = time.perf_counter()
# code...
t1 = time.perf_counter()
print(t1 - t0)
import timeit
timeit.timeit("sum(range(1000))", number=10000)