Unravelling `del`

In my post on unravelling the global statement, I mentioned how after my PyCascades 2023 talk some people came up to me about a couple of pieces of Python syntax that I had not managed to unravel. Beyond global, people thought I should be able to get rid of the del statement. Turns out that a bit of extra work I did in my global statement post lets me unravel del as well!

In my global statement post I talk about the (roughly) 3 namespaces that Python has:

  1. Local
  2. Global
  3. Built-in

In order to support del, I need to figure out how delete names from the local and global namespaces (you will trigger a NameError if you try and delete something in the built-in namespace).

Deleting from the local namespace

To delete a name in the local namespace, you need to make sure that if someone tries to use that same name later on, it causes an UnboundLocalError to be raised. Since there's no way to directly manipulate the local namespace, we can assign a marker to tell us that a local name has been "deleted" (this also allow for garbage collection as expected). So del A can become _DELETED = object(); A = _DELETED. But we also have to detect if A is used after deletion to raise UnboundLocalError as appropriate.

_DELETED = object()

# `del A`
# Referencing `A`
if A is _DELETED:
    raise UnboundLocalError("cannot access local variable 'A' where it is not associated with a value")
Unravelling del A along with accessing the name later on

You might be wondering why are we checking for our _DELETED marker when we can look at some code and know that del A was called, effectively making the A name (seem) useless going forward? Well, think about what happens if A was deleted in an if block? That would mean A could be deleted, but not necessarily deleted unconditionally.

Because local names can be used in expressions, we do have to unravel uses of potentially deleted names just like assignment expressions had use to make new names appear appropriately. It's a bit tedious, but it does mean Python's rules around evaluation is upheld while still allowing for del to operate appropriately.

Deleting from the global namespace

In the global unravelling post, I talked about how to identify when a name was global or built-in compared to a local name. That's important here because deleting a global name is like deleting a key from a dictionary thanks to the globals() function: globals().__delitem__("A"). And if a name happens to not to be in that global dictionary, then NameError should be raised since you can't directly delete something from the built-in namespace.

    gettattr(globals(), "__delitem__")("A")
except KeyError:
    raise NameError("name 'A' is not defined")
Unravelling del A for a global name