Skip to content

Null Object Pattern — Avoid Null Checks with Default Behavior

DodaTech Updated 2026-06-24 3 min read

What You'll Learn

You will learn how the Null Object pattern eliminates null pointer exceptions and repetitive null checks by providing a do-nothing, harmless implementation of an interface.

Why It Matters

Null checks clutter code and are easy to forget. A single forgotten check causes a NullPointerException at runtime. Null Object replaces null references with an object that conforms to the expected interface but implements all methods as no-ops. Instead of writing if logger: logger.info("...") everywhere, you always have a valid logger object and call methods unconditionally.

Real-World Use

Logging frameworks provide a NullLogger implementation that discards all log messages — useful for libraries where the consumer may not want logging. DodaTech's plugin system returns a NullPlugin for missing optional dependencies, allowing the core to call plugin methods unconditionally. If a plugin is optional and not installed, the NullPlugin satisfies all interface contracts without requiring null checks at every call site.

The Pattern

AbstractObject declares the interface. RealObject provides meaningful behaviour. NullObject provides neutral, harmless behaviour.

from abc import ABC, abstractmethod

class Logger(ABC):
    @abstractmethod
    def info(self, message: str) -> None:
        pass

    @abstractmethod
    def error(self, message: str) -> None:
        pass

    @abstractmethod
    def warn(self, message: str) -> None:
        pass

class ConsoleLogger(Logger):
    def info(self, message: str) -> None:
        print(f"[INFO] {message}")

    def error(self, message: str) -> None:
        print(f"[ERROR] {message}")

    def warn(self, message: str) -> None:
        print(f"[WARN] {message}")

class NullLogger(Logger):
    def info(self, message: str) -> None:
        pass

    def error(self, message: str) -> None:
        pass

    def warn(self, message: str) -> None:
        pass

class Customer:
    def __init__(self, name: str, logger: Logger = None):
        self.name = name
        self._logger = logger if logger else NullLogger()

    def purchase(self, item: str, price: float):
        self._logger.info(f"{self.name} purchased {item} for ${price}")
        self._logger.warn("This is a simulation")
real = Customer("Alice", ConsoleLogger())
real.purchase("Book", 12.99)

silent = Customer("Bob")
silent.purchase("Pen", 1.99)
print("No output above — NullLogger suppressed all messages")
[INFO] Alice purchased Book for $12.99
[WARN] This is a simulation
No output above — NullLogger suppressed all messages

Structure

classDiagram
    class AbstractObject {
        <>
        +operation()
    }
    class RealObject {
        +operation()
    }
    class NullObject {
        +operation()
    }
    class Client {
        -dependency: AbstractObject
        +Client(dependency: AbstractObject)
    }
    RealObject ..|> AbstractObject
    NullObject ..|> AbstractObject
    Client --> AbstractObject : uses

Real-World Usage

  • Python io.StringIO when stderr is suppressed — a null-dev Null Object pattern in logging.
  • Java java.util.logging.Logger — loggers can be disabled by setting a no-op handler.
  • .NET System.Net.WebClient with null credentials — a Null Object pattern for authentication.
  • Apache Commons NullOutputStream — an output stream that discards all written data.
  • Rails ActiveSupport::Notifications with a no-op subscriber — silences instrumentation.
  • Strategy is similar — Null Object is a specific Strategy that does nothing.
  • State may use a null state that ignores all requests.
  • Proxy can be implemented as a null proxy that passes through without side effects.
  • Singleton often provides the shared NullObject instance.

Pros and Cons

Pros Cons
Eliminates null checks and NPE risk May hide bugs that would be caught by early failure
Simplifies client code — no branching on null The NullObject may violate the interface contract
Unconditional method calls improve readability Not suitable when null has domain meaning (e.g., "not found")
Easy to add — implement the same interface Overuse can make debugging harder
Centralises "do nothing" behaviour Returning NullObject when null is expected can break existing null checks

The key insight: the Customer class never checks whether its logger is null. The NullLogger satisfies the interface with no-op methods. If later you decide to add logging, you pass a ConsoleLogger and the customer's purchase method works unchanged. The absence of null checks makes the code cleaner and eliminates an entire category of runtime errors.

Practice Questions

  1. Implement a NullCommand in a command pattern system that does nothing on execute and undo.
  2. When would returning a NullObject be worse than raising an exception? Consider a "user not found" scenario.
  3. How would you distinguish between "no logger configured" (NullObject) and "critical logging failure" (exception)?
  4. Implement a generic NullObject Factory using Python's __getattr__ magic method.

Challenge

Implement a NullIterator that implements the Iterator protocol and immediately raises StopIteration on the first call. Where would this be useful?

Real-World Task

Run DodaTech's exception tracker on your application to find the most common null-pointer crash sites. Rank them by frequency and implement Null Objects for the top three.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro