Python
len function
performance
time complexity
programming optimization

Cost of len function

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Python's built-in len() function runs in O(1) constant time for all built-in container types (list, tuple, dict, set, str, bytes). It does not iterate over elements — it reads a pre-stored length field from the object's C struct. This makes len() effectively free and safe to call in loops, conditionals, and hot paths without performance concerns.

How len() Works Internally

Every built-in Python container stores its size as an integer field in the underlying C struct. When you call len(obj), CPython calls obj.__len__(), which reads this field directly.

python
1# All O(1) — reads a stored integer, no iteration
2my_list = [1, 2, 3, 4, 5]
3len(my_list)      # Reads ob_size from PyListObject → 5
4
5my_dict = {"a": 1, "b": 2}
6len(my_dict)      # Reads ma_used from PyDictObject → 2
7
8my_str = "hello world"
9len(my_str)       # Reads ob_size from PyUnicodeObject → 11
10
11my_set = {1, 2, 3}
12len(my_set)       # Reads used from PySetObject → 3

The CPython source for list.__len__ is essentially:

c
1// Objects/listobject.c (simplified)
2static Py_ssize_t list_length(PyListObject *a) {
3    return Py_SIZE(a);  // Macro that reads a->ob_size
4}

Comparison with Other Languages

LanguageOperationTime Complexity
Pythonlen(x)O(1) for all built-in types
Javalist.size(), map.size()O(1)
Javaarray.lengthO(1) — field access
Cstrlen(s)O(n) — scans for null terminator
Golen(x)O(1) for slices, maps, strings, channels
JavaScriptarr.lengthO(1) — property access
Rustvec.len()O(1) — stored field

The notable exception is C's strlen(), which must scan the entire string to find the null terminator. Python's len() on strings does not have this cost.

Custom len Method

You can define __len__ on your own classes to support len():

python
1class TaskQueue:
2    def __init__(self):
3        self._items = []
4
5    def __len__(self):
6        return len(self._items)  # O(1) — delegates to list
7
8    def __bool__(self):
9        return len(self._items) > 0
10
11queue = TaskQueue()
12print(len(queue))  # 0
13print(bool(queue)) # False

The cost of len() on custom objects depends entirely on your __len__ implementation. If it iterates, it becomes O(n):

python
1class FilteredView:
2    def __init__(self, data, predicate):
3        self._data = data
4        self._predicate = predicate
5
6    def __len__(self):
7        # O(n) — must iterate to count matching elements
8        return sum(1 for x in self._data if self._predicate(x))

Using len() in Loops and Conditionals

Since len() is O(1), these patterns are all efficient:

python
1# Fine — len() is O(1), no need to cache
2while len(queue) > 0:
3    process(queue.pop())
4
5# Fine — checking emptiness
6if len(my_list) == 0:
7    print("empty")
8
9# Pythonic alternative — uses __bool__ or __len__
10if not my_list:
11    print("empty")
12
13# Fine in loop conditions
14for i in range(len(my_list)):
15    print(my_list[i])

There is no performance benefit to caching len() in a variable for built-in types:

python
1# Unnecessary — len() is already O(1)
2n = len(my_list)
3for i in range(n):
4    print(my_list[i])
5
6# Equivalent performance
7for i in range(len(my_list)):
8    print(my_list[i])

len() vs Truthiness for Empty Checks

Both approaches work for checking emptiness, but the Pythonic convention is to use truthiness:

python
1# Pythonic — preferred by PEP 8
2if my_list:
3    process(my_list)
4
5if not my_dict:
6    return default
7
8# Also correct but less idiomatic
9if len(my_list) > 0:
10    process(my_list)

Both are O(1). The truthiness check calls __bool__ first; if not defined, it falls back to __len__ and checks if the result is nonzero.

Generators and Iterators Have No len()

python
1gen = (x * 2 for x in range(1000))
2len(gen)  # TypeError: object of type 'generator' has no len()
3
4# Must consume the generator to count
5count = sum(1 for _ in gen)  # O(n) and exhausts the generator
6
7# range objects DO support len() — O(1)
8r = range(1_000_000)
9len(r)  # 1000000 — computed from start/stop/step, not by iteration

Common Pitfalls

  • Assuming len() is O(n) and caching it unnecessarily: len() is O(1) for all built-in types. Writing n = len(lst) before a loop adds a variable for no benefit. The only reason to cache is if the list changes during the loop and you need the original length.
  • Calling len() on a generator: Generators do not have a length because they produce values lazily. Calling len() raises TypeError. Convert to a list first with list(gen) or use sum(1 for _ in gen) (but this consumes the generator).
  • Implementing slow len on custom classes: If your __len__ iterates over data, len(obj) becomes O(n). This breaks the expectation that len() is cheap. Maintain a counter that updates on add/remove instead.
  • Using len() to check for None: len(x) > 0 raises TypeError if x is None. Check if x is not None and len(x) > 0 or use if x which handles None gracefully (since None is falsy).
  • Confusing len() with sys.getsizeof(): len() returns the number of elements. sys.getsizeof() returns the memory size in bytes. They measure different things — a list with 1000 small integers has len() of 1000 but getsizeof() around 8056 bytes.

Summary

  • len() is O(1) for all built-in Python types — it reads a stored integer, never iterates
  • CPython containers (list, dict, set, str, tuple, bytes) store their size in the underlying C struct
  • No need to cache len() in a variable for performance — call it freely
  • Custom __len__ methods determine the cost for user-defined classes — keep them O(1)
  • Generators and iterators do not support len() — use sum(1 for _ in gen) to count
  • Use truthiness (if my_list:) rather than len(my_list) > 0 for idiomatic empty checks

Course illustration
Course illustration

All Rights Reserved.