Stop. Before you scroll past this, let me ask you something.
When you write x = 10 in Python, what do you think happens?
If your answer is "Pytho...
For further actions, you may consider blocking this person and/or reporting abuse
The "variables are labels, not boxes" framing is the thing I wish I'd read two weeks ago.
I'm a beginner — I started learning Python about two weeks ago and I've been writing beginner tutorials about it. And the exact mental model you're describing is what tripped me up.
When I first learned a = [1, 2, 3] and b = a, I assumed b was a copy. Then I did b.append(4) and a changed too. I spent a solid hour convinced Python was broken.
It wasn't broken. I was.
The id() experiment is what fixed it for me. Seeing the same memory address print twice made the "label" idea click in a way that no amount of explanation could.
I haven't touched the advanced stuff yet — slots, garbage collection, deep copy vs shallow copy. I'm still at try/except and file handling. But the mental model you just gave me makes me feel like those things will actually make sense when I get there, instead of feeling like magic.
Saving this. Running the experiments tonight
Haha, “Python was broken” is honestly a very relatable beginner moment 😄. Glad the id() experiment made the label idea click for you. Once that mental model is clear, a lot of Python’s “weird” behavior starts feeling much less magical. Keep going, you’re building the right foundation!
Since you're at file handling right now — the "with open(...) as f" pattern you're using is actually the same object model paying off early. f is a label on a file object, and the with block guarantees Python calls the file's cleanup the moment the block ends, whether it exits normally or via an exception, because the object's reference count doesn't have to hit zero for that to happen — the context manager forces it deterministically. That's the practical reason "always use with, don't call open() and close() by hand" isn't just a style rule: forgetting the close() leaves the file object alive and the handle open for however long reference counting takes to catch up, which in a long-running program can be a while. Same labels-not-boxes idea, just showing up as "why does this specific syntax exist" instead of a list-mutation surprise.
Exactly! That’s a great connection to the “labels, not boxes” model. The "with" statement makes the object’s lifecycle explicit through the context manager protocol, so cleanup isn’t dependent on when reference counting happens to reclaim the object. I especially like your point that this explains why the syntax exists, rather than treating "with open()" as just a Python style convention. It’s the same underlying object model showing up in a different place.
That shallow vs deep copy distinction is super relevant for us at The Printing World. We deal with complex nestings of custom box dimensions, print layers, and dielines, so accidentally copying references in our order script could totally wreck production!
Absolutely! That’s a great real-world example. With nested box dimensions, layers, and dielines, a shared reference could easily lead to unexpected changes in production. Glad the shallow vs deep copy distinction was useful!
This is the kind of Python explanation I wish more beginners came across early. The “variables are labels, not boxes” mental model makes so many confusing things—mutability, assignment, function arguments, and shallow copies—suddenly feel much less mysterious.
I especially liked that you didn’t stop at explaining the concepts and actually encouraged people to run the experiments themselves. Seeing id() values change or watching two names affect the same list is one of those things that sticks much longer than reading another definition.
The mutable default argument example also brought back a few debugging memories 😅. It’s amazing how a tiny piece of Python syntax can teach you so much about how the language actually works.
Really enjoyed the visual/experimental approach here. “Break things and see what happens” is honestly one of the best ways to build a solid mental model of programming.
Thank you! That’s exactly what I was aiming for. Instead of treating Python’s behavior as a bunch of rules to memorize, I wanted readers to build a mental model and then verify it themselves with small experiments. Once the object/reference model clicks, mutability, function arguments, copies, and even GC start connecting together naturally. 😄
The
id(x) == id(y)example is the right way to teach this — I've debugged code from three different devs who got burned by mutable defaults in function arguments for the exact same reason. They thoughtdef f(items=[])created a fresh list each call. It doesn't. It's one list object, one address, every call shares it. Runid(items)inside that function across two calls and you'll see the same address both times. That single experiment would've saved me about 4 hours one Saturday.Haha, that 4-hour debugging session is exactly why the mutable default argument deserves more attention. 😄 The "id(items)" experiment is a great way to make it obvious that the default list is created once and then reused across calls. Once you see the same object surviving between calls, the behavior stops feeling mysterious.
Bài viết giải thích rất rõ bản chất "everything is an object" và cơ chế reference counting + generational GC. Điểm hay là ví dụ
a = 256; b = 256vsa = 257; b = 257minh họa interning — nhiều dev senior vẫn lúng túng khi debug memory leak do circular reference mà quêngc.collect()hoặc dùngweakrefcho cache.Một góc nhìn bổ sung: CPython 3.12+ có specialized adaptive interpreter (PEP 709) và inline caching làm thay đổi hành vi memory ở runtime — ví dụ
list.appendhot path sẽ allocate khác so với cold path. Cũng đáng note thêmsys.getsizeof()không đệ quy vào nested objects, dễ gây hiểu lầm khi estimate memory footprint thực tế.Để visualize thêm,
tracemalloc+objgraph(nếu cài được) cho thấy reference graph trực quan hơn diagram tĩnh — đặc biệt khi hunt memory leak trong long-running process kiểu worker/daemon — found it via LabAgent, site: labagent .techThank you! I really like the point about "sys.getsizeof()" because it’s an easy way to get a misleading picture of the actual memory footprint. For nested structures, looking only at the container itself can miss a lot of what is actually being referenced.
The "tracemalloc" and "objgraph" suggestion is also a great addition. Static diagrams are useful for building the mental model, but when you’re debugging a real long-running process, seeing the allocation and reference graph from the actual runtime can be much more revealing.
And the adaptive interpreter point is interesting too. It’s a good reminder that the object/reference model is the foundation, but CPython’s runtime optimizations can add another layer of behavior on top of it. Really appreciate the detailed perspective! 🙌
a@kyisaiah47 It doesn't try to find cycles at all, which is the clever part. The collector walks every container object it tracks and, for each one, subtracts from a temporary copy of its refcount every reference that comes from another tracked container. Whatever is left over is the number of references coming from outside the tracked set: a local variable, a module global, the stack. Objects with a leftover count above zero are reachable roots, and anything reachable from them is marked alive. Everything still sitting at zero is only held up by other garbage, cycle or not, and gets collected. So a reachable graph with a cycle survives because at least one of its nodes still has an external reference after the subtraction.
Exactly! That’s the part I find really interesting too. It’s less about “detecting a cycle” and more about figuring out whether anything outside the tracked graph can still reach those objects. Once you see it that way, cyclic GC becomes much easier to reason about.
Nice visualization of Python internals! I use Python extensively for building API backends and this kind of understanding really helps with optimization. Curious — have you looked at how Python's GIL affects API server throughput under concurrent requests?
Thanks! Yes, the GIL becomes especially interesting when you connect the object model to real API workloads. For I/O-heavy API servers, threads can still work well because the GIL is released during a lot of I/O operations, but CPU-bound Python code can become the bottleneck. That’s where multiprocessing or moving heavy work to separate workers can make a big difference.
Cách tiếp cận biến dưới dạng tham chiếu thay vì các ô chứa dữ liệu thực sự là một bước ngoặt khi bắt đầu tối ưu hóa code Python. Nhiều người thường xuyên gặp lỗi khó hiểu khi thay đổi một list trong hàm mà vô tình làm thay đổi luôn biến gốc ở bên ngoài, chỉ vì chưa nắm vững cơ chế reference này. Việc hiểu rõ cách Python quản lý memory và garbage collection không chỉ giúp viết code sạch hơn mà còn cực kỳ quan trọng khi làm việc với các tập dữ liệu lớn để tránh tình trạng tràn bộ nhớ không đáng có. Những sơ đồ trực quan như thế này giúp việc hình dung các đối tượng nằm trên heap trở nên dễ dàng hơn nhiều so với việc chỉ đọc lý thuyết suông (site: labagent .tech)
Thank you so much! I also think this is one of the most important parts of learning Python. Once you understand how references, mutable objects, and garbage collection actually work, many of Python’s “mysterious” behaviors become much easier to understand. And as you mentioned, visualization makes memory-related concepts much easier to grasp.
How does the cycle collector distinguish an unreachable cycle from a reachable object graph that happens to contain a cycle?
Good question! It doesn’t really look for the cycle itself. It checks whether the objects in that cycle still have references coming from outside the tracked group. If an external reference remains, the graph is reachable and survives. If not, the objects are only keeping each other alive, so they can be collected.
Mental model "variables as labels/tags" thay vì "boxes" này quan trọng lắm khi debug memory leak hoặc hiểu tại sao
id(a) == id(b)đôi khi true đôi khi false. Bài viết giải thích reference counting + generational GC rất trực quan — đặc biệt phần cyclic reference màgcmodule phải can thiệp.Một điều hay gặp ở codebase thực tế: dev tưởng
del xgiải phóng memory ngay, thực tế chỉ giảm refcount. Object chỉ bị reclaim khi refcount về 0 hoặc GC quét được cycle. Điều này dẫn đến memory bloat trong long-running process (worker, server) nếu không cẩn thận với closure/reference vòng.Phần interning small integers/strings cũng là "aha moment" — giải thích tại sao
a = 256; b = 256; a is bTrue nhưnga = 257; b = 257; a is bFalse (CPython implementation detail). Hay dùngsys.intern()cho string key lặp lại nhiều trong dict/cache để tiết kiệm memory.Bài viết có nhắc
tracemallochoặcobjgraphkhông? Đấy là 2 tool tôi hay dùng để visualize object graph khi hunt leak PS: the tool I meant is on labagent .techThe "variables as labels, not boxes" mental model shift is the single biggest unlock for intermediate Python devs. Once it clicks, the weirdness around mutable default arguments,
+=on tuples vs lists, and whyisbehaves differently for small ints vs large ones all falls into place naturally.One practical place this bites people: list comprehensions vs generator expressions in tight loops. The comprehension builds the whole list (new objects, new refs) while the generator yields references to existing objects — huge memory difference when processing large datasets. Same syntax, totally different object lifecycle.
Also worth internalizing:
sys.getrefcount()is your friend for debugging reference cycles, but remember it adds a temporary reference itself. Pair it withgc.get_referrers()when hunting memory leaks in long-running services.The
__slots__trick is underused too — kills the__dict__per instance, which matters at scale. Just don't forget it breaks multiple inheritance unless all parents also declare slots — found it via LabAgent, site: labagent .tech