A Python Pitfall: How Implementing __len__ Changed an Object's Truthiness
Hello!
When writing custom classes in Python,
have you ever run into the baffling situation where "the object clearly exists, but an if statement judges it False"
?
In this article, we explain the unexpected truthiness behavior that arises when you implement the __len__ method!
A Bug We Actually Ran Into
It happened while implementing a class that manages user posts.
class PostManager:
"""A class that manages blog posts"""
def __init__(self, user_id):
self.user_id = user_id
self._posts = []
self._cache = {}
def __len__(self):
"""Return the number of posts"""
return len(self._posts)
def add_post(self, post):
self._posts.append(post)
self._cache[post.id] = post
# Usage example
manager = PostManager(user_id=123)
# Check that the manager is valid (or so we intended)
if manager:
print("Adding a post")
else:
print("Manager is invalid") # Somehow this branch runs!
The object most certainly existed, yet somehow it was evaluated as False.
Why This Happens
Python's truthiness evaluation works like this:
# When `if obj:` runs, Python evaluates in the following order
# 1. Check whether obj.__bool__() is defined
# 2. If not, check whether obj.__len__() is defined
# 3. If len(obj) == 0, evaluate as False
In other words, the moment you implement the __len__ method, the class is treated as a "container type," and whenever it has zero elements it evaluates to False!
Which means that a fuzzy truthiness check like if obj: is genuinely dangerous.
Concrete Problem Examples
Example 1: A database connection class
class DatabaseSession:
"""A class that manages a database session"""
def __init__(self, connection_string):
self.connection_string = connection_string
self._query_cache = []
self._is_connected = True
def __len__(self):
"""Return the number of cached queries"""
return len(self._query_cache)
def execute(self, query):
result = self._execute_query(query)
self._query_cache.append(query)
return result
# Create a session
session = DatabaseSession("postgresql://localhost/mydb")
# Meant as a connection check, but...
if session:
session.execute("SELECT * FROM users") # Never runs!
else:
print("Session is invalid") # This branch runs
This example exhibits an even more serious problem.
The database session is actually valid, and internally the _is_connected flag is set to True — the connection to the database has been established. However, because the __len__ method is implemented to return the size of the query cache, it always returns 0 in the initial state, before any query has run. As a result, a perfectly valid database session is treated as "invalid," and query execution is blocked. In a real application, this can turn into the fatal bug of database operations not working at all.
Example 2: Initializing a task queue
class TaskScheduler:
"""A task scheduler"""
def __init__(self, max_workers=4):
self.max_workers = max_workers
self._pending_tasks = []
self._completed_tasks = []
def __len__(self):
"""Return the number of pending tasks"""
return len(self._pending_tasks)
def schedule(self, task):
self._pending_tasks.append(task)
# Initialize the scheduler
scheduler = TaskScheduler()
# Check that the scheduler is valid
if scheduler: # False (no tasks yet)
scheduler.schedule(initial_task) # Never runs
The task scheduler example contains a logical contradiction. Immediately after creating the scheduler there are naturally no tasks registered — a perfectly normal state. But "is the scheduler valid?" and "are there any tasks?" should be two separate concepts. With this implementation, the scheduler evaluates as False whenever it has no tasks, so you cannot even add the first task. You end up with an unsolvable loop: "the scheduler is not valid until it has tasks, but you cannot add tasks until the scheduler is valid."
Example 3: A case that makes debugging painful
class EventLogger:
"""A class that manages event logs"""
def __init__(self, log_file):
self.log_file = log_file
self._events = []
def __len__(self):
return len(self._events)
def log(self, event):
self._events.append(event)
# Initialize the logging system
logger = EventLogger("/var/log/app.log")
# Debug info
print(logger) # <EventLogger object at 0x...>
print(logger is not None) # True
print(bool(logger)) # False (!)
print(f"Event count: {len(logger)}") # Event count: 0
# Unexpected behavior in a conditional
if logger:
logger.log("Application started") # Never runs
The EventLogger example causes particular confusion during debugging. Running print(logger) prints the object's memory address, so it plainly exists. logger is not None also returns True, so the None check passes too. Yet running bool(logger) returns False — a seemingly contradictory result. This inconsistency forces developers to burn extra time figuring out why the code will not run. Worse, since not even the first event can be recorded, the logger's basic function is broken from the start. The application startup log — the most important information of all — never gets written: a fatal flaw.
The Common Pattern Behind These Problems
What these examples share is a design that is guaranteed to fail in its initial state.
Having zero elements is a perfectly normal initial state for many container-type objects, but that does not mean the object as a whole is "invalid".
Moreover, two concepts that should remain distinct — whether the object exists and whether it has contents — get conflated by Python's implicit truthiness conversion. And when the developer's intent and Python's behavior diverge, readability suffers badly and future maintenance becomes harder.
These problems are not mere technical bugs — they stem from a fundamental design misunderstanding. When implementing the __len__ method, you must fully understand that the class will then be treated as a Python container type, and design accordingly.
How to Handle It Properly
Method 1: Explicit existence checks
# Not recommended
if manager:
process_posts(manager)
# Recommended
if manager is not None:
process_posts(manager)
The simplest and most reliable fix is an explicit existence check using is not None. What makes this approach excellent is that it completely bypasses Python's implicit truthiness conversion.is not None checks only whether the object is truly None, so it is unaffected by any __len__ method implementation. Whether the object is empty or holds a hundred elements, it returns True as long as the object exists.
This fix is especially useful when problems surface after adding a __len__ method to an existing codebase. The change is localized, and the intent — "does this object exist?" — is unambiguous. The catch is that the whole team must apply the rule consistently, and violations are easy to miss in code review.
Method 2: Implementing the __bool__ method
class PostManager:
def __init__(self, user_id):
self.user_id = user_id
self._posts = []
self._is_active = True
def __len__(self):
return len(self._posts)
def __bool__(self):
"""Return whether this manager is active"""
return self._is_active # Judged by active state, regardless of post count
__bool__ — implementing this method gives you complete control over the truthiness logic. When the __bool__ method is defined, Python uses it in preference to __len__. In this example, even with zero posts, as long as the _is_active flag is True the manager evaluates to True.
The advantage of this approach is that the object's "validity" can be defined independently of the post count. It also covers cases where you want to disable the manager for reasons unrelated to posts, such as maintenance mode or an error state. In the database connection example, having the __bool__ method return the connection state means the session is correctly judged "connected" even when the query cache is empty.
That said, this approach has caveats. If len() and bool() give inconsistent results — say, "five elements yet False" or "zero elements yet True" — readers of the code may be confused. For this reason, when implementing the __bool__ method, it is important to document its decision logic clearly.
Method 3: Explicit state-checking methods
class TaskScheduler:
def __init__(self, max_workers=4):
self.max_workers = max_workers
self._pending_tasks = []
self._is_running = False
def __len__(self):
return len(self._pending_tasks)
def is_empty(self):
"""Return whether the task queue is empty"""
return len(self._pending_tasks) == 0
def is_running(self):
"""Return whether the scheduler is running"""
return self._is_running
def has_pending_tasks(self):
"""Return whether there are pending tasks"""
return len(self._pending_tasks) > 0
Providing dedicated methods is the clearest approach, leaving no room for misunderstanding. Method names like is_empty(), is_running(), and has_pending_tasks() state exactly what each one checks. With this approach, you express your intent precisely without relying on implicit truthiness conversion.
The biggest benefit is a dramatic gain in readability. Instead of the ambiguous if scheduler:, writing concrete conditions such as if scheduler.is_running(): or if scheduler.has_pending_tasks(): lets readers see immediately what is being checked. And because different states can be checked independently, complex situations like "the scheduler is running but the task queue is empty" can be expressed precisely.
This approach also makes tests easier to write. Since each method has a single responsibility, each can be tested independently, and edge cases become simple to verify. When debugging, it is likewise easier to pinpoint which condition prevented the code from running.
The Pitfall in AI-Generated Code
A dangerous pattern that code-generation AI loves
Today's code-generation AI frequently suggests the concise if obj: style.
At first glance it looks Pythonic and polished, but it hides a major pitfall.
# Code AI often generates
def process_data(manager):
if manager: # AI favors brevity
return manager.get_data()
return None
# The problem that actually occurs
manager = DataManager() # Newly created; data still empty
result = process_data(manager) # Returns None (unintended)Why does AI tend to generate code like this? Because much of its training data uses this pattern in the context of a None check.
Across the vast code examples on GitHub and Stack Overflow, if obj: is commonly used to None-check arguments, and AI has learned it as a "good pattern." But since AI does not fully understand context, it may apply the same pattern to classes that implement the __len__ method.
Practical safeguards when using AI-generated code
It also pays to craft your prompts carefully. Including concrete instructions such as "use is not None for None checks" and "check object validity with a dedicated method" leads the AI to generate safer code.
# Example of a better prompt
"""
Please create a database connection class.
- Always use `is not None` for existence checks
- Judge object validity with the is_connected() method
- Do not use implicit truthiness checks like `if self:`
"""AI tools are powerful development aids, but accepting the code they generate uncritically is genuinely risky.
Especially with classes that implement the __len__ method, always keep in mind that the "common patterns" AI has learned can trigger unintended behavior. In code review, watch for these latent issues regardless of whether the code came from an AI.
Best Practices
__len__: when implementing it, recognize that the class will behave as a container
Document explicitly that "the object evaluates as False when empty"
Make the intent clear with type hints and docstrings
class EventLogger:
"""A class that manages event logs
Note: because this class implements __len__,
bool(logger) == False when there are zero events.
"""
Implement the __bool__ method as well, when needed
def __bool__(self):
return self._is_initialized and self._is_connected
Provide a dedicated method for emptiness checks
if not manager.is_empty():
# Handle the case where posts exist
Use is not None for existence checks
if scheduler is not None:
# Handle the case where the scheduler exists
Summary
We have walked through the pitfalls of implementing Python's __len__ method.
What deserves particular caution is that today's code-generation AI likes to suggest the concise if obj: style. ChatGPT and GitHub Copilot frequently produce this dangerous pattern as the "Pythonic way" they learned from their training data. But because AI does not sufficiently account for the side effects of an existing __len__ method, using the generated code as-is can lead to unexpected defects.
As countermeasures: use is not None explicitly for existence checks; implement the __bool__ method to customize truthiness when needed; and — most reliably of all — provide clearly named methods such as is_empty() and is_valid(). With these in place, the code's intent becomes clear and future maintenance that much easier.