Five ways to purge None checks from your Python domain core
The Core Problem with None Spreads
None seems harmless at first. It acts as a quick, convenient placeholder when data is missing or operations fail. However, once None slips into your application's domain core, it acts like a virus. Every function, class, and method that interacts with that data must run continuous defensive validation checks.
This architecture pattern leads to highly verbose, unreadable code. The issue is not that raw, external data contains empty values. The issue is letting that messy data penetrate your internal business logic without strict validation. To clean up your architecture, you must push safety checks to the boundaries of your system and keep your core domain strict and predictable.
Prerequisites

To get the most out of this tutorial, you should have a solid grasp of intermediate Python concepts. You should be familiar with object-oriented programming (OOP), standard error-handling structures, and typing paradigms. Familiarity with Data Classes is highly recommended.
Key Libraries & Tools
- Python Standard Library: This refactoring relies heavily on built-in tools like
dataclasses(specificallyfieldanddefault_factory) and typing tools likeProtocol. - Returns Package: A popular community package that provides functional result types, though standard exceptions are preferred for standard Python codebases.
Refactoring Away From None
Let's break down the primary strategies to clean up your architecture by refactoring a messy drone delivery system.
Use Better Defaults
If a None value represents an empty state, nothing to do, or a disabled feature, replace it with a valid default value. Instead of checking for None before iterating, use an empty collection.
from dataclasses import dataclass, field
@dataclass
class Delivery:
# Bad: avoid_zones: list[GeoFence] | None = None
# Better: Use an empty list by default
avoid_zones: list[GeoFence] = field(default_factory=list)
Push Validation to the Edge
Separate raw, messy data structures at your application's boundaries from your strict internal domain objects. Validate data once as it enters your system, then pass around guaranteed types.
@dataclass(frozen=True)
class ReadyDrone:
drone_id: str
location: Coordinates
battery_level: int
max_wind_speed: float
def prepare_drone(telemetry: RawTelemetry) -> ReadyDrone:
if telemetry.location is None:
raise ValueError("Drone has no GPS location")
if telemetry.battery is None:
raise ValueError("Drone has no battery data")
return ReadyDrone(
drone_id=telemetry.drone_id,
location=telemetry.location,
battery_level=telemetry.battery,
max_wind_speed=telemetry.max_wind_speed
)
Replace None Returns with Exceptions
Instead of returning None when a function cannot fulfill its purpose, raise an explicit, domain-specific exception. This forces callers to handle the failure explicitly or let it propagate, avoiding silent errors.
class RouteRejected(Exception):
pass
def assign_delivery(drone: ReadyDrone) -> Route:
if drone.battery_level < 20:
raise RouteRejected("Battery level too low")
return Route(destination=drone.location)
The Null Object Pattern
When "doing nothing" is a valid and expected behavior, implement a Null Object. By implementing a common interface that performs no operation, you remove the need for repetitive conditional checks.
from typing import Protocol
class Diagnostics(Protocol):
def record(self, message: str) -> None:
...
class ActiveDiagnostics:
def record(self, message: str) -> None:
print(f"LOG: {message}")
class NullDiagnostics:
def record(self, message: str) -> None:
pass # Do absolutely nothing
Syntax Notes
Using Protocol from the typing module allows you to implement structural subtyping (duck typing) in Python. Your classes do not need to explicitly inherit from the protocol class; they only need to implement the defined methods. This makes it incredibly easy to swap a real logging class for a NullDiagnostics helper.
Practical Examples
These patterns apply to many software domains outside of drone dispatch systems:
- API Integration: Translating loose, missing-key JSON payloads into strict domain entities using Pydantic or dataclasses at the controller layer.
- Feature Flagging: Using a Null Feature Flag client that defaults all flags to
Falsein local development without injecting mock checks into your code.
Tips & Gotchas
- Do Not Abuse Result Objects: While functional programming structures like
Resulttypes (success or failure wrappers) are popular in Rust, they can confuse traditional Python developers. Standard custom exceptions are much more idiomatic. - Failing Fast: Always fail fast. Crash immediately at your API boundaries rather than letting a corrupted state slip deep into your execution thread.
- Python
- 50%· products
- Data Classes
- 10%· products
- None
- 10%· products
- Pydantic
- 10%· products
- Returns Package
- 10%· products
- Tony Hoare
- 10%· people

Stop Checking for None Everywhere
WatchArjanCodes // 20:32
On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!