`None`: el singleton que representa ausencia de valor

En Python, None no es simplemente “nada” — es un objeto concreto, el único ejemplar de la clase NoneType, y existe exactamente una copia de él en todo el proceso. Eso lo convierte en un singleton: no importa cuántas veces escribas None en tu código, siempre estás haciendo referencia al mismo objeto en memoria.

type(None)   # <class 'NoneType'>
id(None)     # siempre el mismo número, en cualquier parte del programa

Python lo diseñó así para representar ausencia de valor — no un número, no un texto vacío, no un cero — sino la ausencia explícita de cualquier valor significativo. Esa distinción importa más de lo que parece a primera vista.

El problema de confundir ausencia con falsedad

None es falsy — en un contexto booleano se comporta como False. Pero esto no lo hace equivalente a False, ni a 0, ni a "". Son cosas completamente distintas con semánticas distintas:

ValorTipoSignifica
NoneNoneTypeausencia de valor
Falseboolel valor booleano falso
0intel número cero
""struna cadena que existe pero está vacía

Todos son falsy, pero mezclarlos causa bugs reales. Imagina una función que devuelve el número de errores encontrados: si devuelve 0, todo está bien; si devuelve None, el análisis no pudo ejecutarse. Son situaciones completamente diferentes, y tu código necesita poder distinguirlas.

Por eso, PEP 8 establece explícitamente que la comparación con None debe hacerse con is, no con ==:

# La identidad confirma que es exactamente el singleton None
if x is None: ...
if x is not None: ...

is comprueba identidad de objeto (mismo lugar en memoria), no igualdad de valor. Como None es un singleton garantizado, is None siempre funciona. == None también funciona la mayoría de las veces, pero una clase puede sobrescribir __eq__ y hacer que una instancia arbitraria se compare como “igual a None” — lo cual es incorrecto e impredecible.

Funciones sin return y None como sentinel

Dos patrones aparecen constantemente en código Python real: las funciones que retornan None implícitamente, y None como valor centinela en parámetros opcionales.

from __future__ import annotations


def find_user(users: list[dict], username: str) -> dict | None:
    """
    Retorna el usuario si existe, None si no se encuentra.
    Distingue 'no encontrado' de cualquier valor válido.
    """
    for user in users:
        if user["name"] == username:
            return user
    # Sin return explícito → Python retorna None automáticamente


def greet(name: str, title: str | None = None) -> str:
    """
    title=None como sentinel: significa 'no se proporcionó título',
    distinto de title="" que significaría 'título vacío intencionalmente'.
    """
    if title is not None:
        return f"Hola, {title} {name}"
    return f"Hola, {name}"


def count_occurrences(items: list, target: object) -> int:
    """Muestra por qué 0 y None son valores distintos y significativos."""
    return items.count(target)


# ── Programa principal ────────────────────────────────────────────────

users = [
    {"name": "alice", "role": "admin"},
    {"name": "bob",   "role": "editor"},
]

result = find_user(users, "alice")
if result is not None:
    print(f"Usuario encontrado: {result}")

result = find_user(users, "ghost")
if result is None:
    print("Usuario no existe en el sistema")

print(greet("García"))              # Sin título
print(greet("García", title="Dr.")) # Con título
print(greet("García", title=""))    # Título vacío intencionalmente — caso diferente

counts = [None, 0, None, False, 0, None]
print(count_occurrences(counts, None))   # → 3, solo los None
print(count_occurrences(counts, 0))      # → 2, solo los ceros (0 y False, porque False == 0)
print(count_occurrences(counts, False))  # → 2, mismo resultado por herencia bool/int

Lo que el código revela

find_user ilustra el contrato más común de None: una función que puede o no encontrar algo devuelve el objeto si lo encuentra, y None si no. El if result is not None posterior es la forma idiomática de manejar ese contrato — compacta, segura, y explícita en su intención.

Fíjate que el último bloque sin return no es un olvido ni un error: Python lo especifica así. Toda función que termina sin ejecutar un return retorna None. Es útil saberlo para funciones de efecto secundario — como las que solo modifican una lista o imprimen algo — donde el valor de retorno genuinamente no existe.

En greet, el parámetro title: str | None = None demuestra el patrón sentinel. Usar None como valor por defecto dice “el llamador no proporcionó este argumento”. Es diferente de "", que dice “el llamador proporcionó explícitamente una cadena vacía”. Si usaras "" como centinela y "" como valor legítimo, ya no podrías distinguir los dos casos. None, al ser de tipo propio, nunca colisiona con ningún valor de negocio.

El experimento con count_occurrences vale la pena inspeccionarlo: None, 0 y False son tres objetos distintos, y list.count los trata como distintos — excepto 0 y False, que sí son iguales según == porque bool es subclase de int en Python. Eso confirma que None vive en una categoría completamente aparte.

Errores que debes conocer

Error: comparar con None usando == en lugar de is, lo que expone el código a clases con __eq__ personalizado.

# ❌ Wrong
class Tricky:
    def __eq__(self, other):
        return True  # "igual" a cualquier cosa, incluido None

obj = Tricky()
if obj == None:   # True — resultado incorrecto e inesperado
    print("Esto no debería ejecutarse")

# ✅ Right
if obj is None:   # False — correcto: obj no es el singleton None
    print("Esto nunca se ejecuta")

is comprueba identidad de objeto, no valor, así que ninguna implementación de __eq__ puede interferir.


Error: usar 0 o "" como centinela cuando esos valores son válidos en el dominio del problema.

# ❌ Wrong
def get_score(database: dict, player: str) -> int:
    return database.get(player, -1)  # -1 como "no encontrado" — pero -1 podría ser una puntuación válida

score = get_score({"alice": -1}, "alice")
if score == -1:
    print("Jugador no encontrado")  # Bug: alice sí existe y su puntuación es -1

# ✅ Right
def get_score(database: dict, player: str) -> int | None:
    return database.get(player)  # None si la clave no existe

score = get_score({"alice": -1}, "alice")
if score is None:
    print("Jugador no encontrado")  # Correcto: distingue ausencia de cualquier valor numérico

None es el único valor que no pertenece al dominio de ningún tipo de negocio, lo que lo convierte en el centinela universal y sin ambigüedades.

23

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio