Python del vs assigning to None

Python's runtime handles memory for you, but every so often you'll see code that tries to give it a nudge. The two ways that come up are:

1x = None

and

1del x

They look interchangeable. They aren't.

What each one does

x = None rebinds the name x to None. Whatever it pointed at before now has one fewer reference; if that was the last one, CPython's reference counter frees the object immediately. The name x is still there — using it gives you None.

del x removes the binding entirely. The name is gone. Touching x afterward raises NameError (or AttributeError if you del obj.attr on an instance attribute). The previously-referenced object also loses a reference and gets freed in the same moment if the count hits zero.

So: = None keeps the name with a tiny None reference; del removes the name as well.

Worth being precise about a couple of things, since the framing on this trips people up:

  • Neither operation "tells the garbage collector" anything. CPython's main memory management is reference counting, not the cyclic GC. The cyclic collector only runs to break reference cycles; for plain refcount-to-zero, freeing is synchronous.
  • Even when an object is freed, CPython's small-object allocator keeps the arena around. Memory rarely returns to the OS until the process exits.

Demonstration

1import sys 2 3x = 'Some text here to give the variable a decent size' 4y = 2 5 6print('x size before:', sys.getsizeof(x), 'bytes') 7 8x = None 9del y 10 11print('x value after:', x) 12print('x size after :', sys.getsizeof(x), 'bytes') 13print('x type after :', type(x).__name__) 14 15if x is None: 16 print('x is still bound, just to None') 17 18print('y after del:', y) # NameError

Output on CPython 3.12:

x size before: 90 bytes x value after: None x size after : 16 bytes x type after : NoneType x is still bound, just to None Traceback (most recent call last): File "demo.py", line 17, in <module> print('y after del:', y) NameError: name 'y' is not defined

A None-bound name carries the 16-byte cost of pointing at the singleton None (which is a value of type NoneType, not a type itself). A deleted name carries no cost — the local slot is empty.

The bytecode

1import dis 2 3def using_none(): 4 x = 1 5 x = None 6 7def using_del(): 8 x = 1 9 del x 10 11dis.dis(using_none) 12print() 13dis.dis(using_del)

On CPython 3.12:

Assigning to None: 0 RESUME 0 2 LOAD_CONST 1 (1) 4 STORE_FAST 0 (x) 6 LOAD_CONST 0 (None) 8 STORE_FAST 0 (x) 10 RETURN_CONST 0 (None) Using del: 0 RESUME 0 2 LOAD_CONST 1 (1) 4 STORE_FAST 0 (x) 6 DELETE_FAST 0 (x) 8 RETURN_CONST 0 (None)

del x is a single DELETE_FAST instruction. x = None is a LOAD_CONST None followed by a STORE_FAST — same shape as any other assignment. (The RESUME 0 at the top of each function is new in Python 3.11; it's the interpreter's reentry point.)

When you'd actually reach for either

Most of the time you don't need to. The cases where it earns its keep:

  • Dropping a large local before a long-running call inside the same function, so the memory can be reused
  • Releasing a reference held by a class attribute or module-level cache
  • Breaking a reference cycle so the cyclic collector can clean it up sooner

For everything else, let the runtime do its job. Reach for del when you genuinely want the name gone; reach for = None when you want the name there but the data not.