Python Performance: Why 'if not list' is 2x Faster Than Using len()
Discover why 'if not mylist' is twice as fast as 'len(mylist) == 0' by examining CPython's VM instructions and object memory access patterns.
In Python, you can check a list for emptiness in two ways:
if not mylist
if len(mylist) == 0
While the 2nd approach is not wrong, the first method is considered more Pythonic. Many people don’t agree, but I’ve already put forward my points in a previous article on that debate.
But this isn’t just about coding style, one of these is ~2x faster than the other, see for yourself:
That's an almost 2x performance difference for something as fundamental as checking if a collection is empty. This operation occurs millions of times in any non-trivial Python application.
The disparity becomes even more intriguing when you consider that both approaches must ultimately determine the same thing: does the collection contain any elements?
What causes this surprisingly large gap? The answer lies in CPython's implementation: where objects live in memory, how the virtual machine processes instructions, and the hidden costs of seemingly simple operations.
In this article, we'll follow both execution paths instruction by instruction, uncovering why a small syntactic difference translates to double the execution time.
TL;DR
If you’re short on time, here are the key insights covered in the article:
In Python, checking if a collection is empty can be done with either
if not mylist
orif len(mylist) == 0
, but the first approach is consistently 2x faster.This performance difference exists because
if not mylist
requires only 2 VM instructions (LOAD_GLOBAL, TO_BOOL), whileif len(mylist) == 0
needs 5 instructions (LOAD_GLOBAL, LOAD_FAST, CALL, LOAD_CONST, COMPARE_OP).Both the TO_BOOL instruction and the len() built-in function check the size of the list by five layers of pointer indirection.
However, the VM optimizes TO_BOOL to TO_BOOL_LIST which is a specialized instruction to evaluate the truth value of list type objects. Because it is optimized for lists, it can do the same thing in a single memory access as compared to five.
Apart from five levels of pointer dereferences, using len() also has the overhead of function calls, which adds up.
For hot loops or performance-critical code, the idiomatic approach (
if not mylist
) is more Pythonic and measurably more efficient.
The rest of the article explores the Python object model, bytecode execution, and internal optimizations to explain exactly why this performance difference exists

Article Structure
We will start by understanding the definition and layout of the list object in CPython. This information is vital for the rest of the article because ultimately both the approaches need to check the size of the list and that information is stored in the object.
After that we will dive into the internals of both the methods of doing emptiness checks and understand what are the expensive parts.
Finally we will summarize by doing a side-by-side comparison of the two approaches.
Understanding the Python List Object Layout
First of all, let’s understand the definition and in-memory layout of the list object in CPython. It will give us insight about how the CPython runtime tracks the size of sequence type objects.
Different object types in CPython need different fields depending on their requirements. For example, a float object needs a double
type field to store its value, while a list type needs to have a dynamic array and its size to store the list data.
But, there are certain common fields that all object types need to have for the CPython runtime to manage them. These include the object reference count, the type information and the size of the object (only needed for collection types, such as lists, dicts, strings).
To make it easier for the CPython runtime to access these common fields in objects of different types, CPython implements an object hierarchy (AKA inheritance model). This ensures that even without knowing the concrete type of the object, the runtime can access these common fields whenever it needs.
The two common fields that every CPython type needs to have are its reference count (ob_refcnt
) and the type (ob_type
) related information. These are kept in a struct called PyObject
which is embedded as the first field in every type definition, making it the object header. As a result of this, any CPython object can be treated as an instance of type PyObject
and the ob_refcnt
count and ob_type
fields can be accessed.
Apart from these fields, if the CPython type is a sequence type, it also needs to provide its current size information to the runtime. Because this size information is not needed for other types, the sequence type objects have a special object header definition called PyVarObject
. It extends PyObject and adds the size field.

A sequence type object has an instance of PyVarObject
as its first field so that it inherits those header fields and then the type can define its own fields after that. For example, the definition of the list type in CPython is shown below.
The following diagram summarizes this whole object model:
As you can see, the list object contains its size information in the ob_size
field (inherited from PyVarObject
). It means that the runtime can always simply lookup this field to know if the list is empty.
With this picture of how the sequence type objects are defined in CPython and how the runtime tracks their size, we are ready to discuss why there is such a stark difference in the performance of the two methods for doing emptiness checks.
Check out my article on PyObject or videos on CPython type system internals for more details on this topic.