Truthiness y falsiness en Python

Python no evalúa condiciones buscando literalmente True o False. Evalúa si un objeto es truthy (se comporta como verdadero en contexto booleano) o falsy (se comporta como falso). Es una distinción sutil pero que permea todo el lenguaje.

El mecanismo interno es simple: cuando escribes if x:, Python llama internamente a bool(x). Y bool(x) sigue un protocolo preciso: primero busca __bool__ en el objeto; si no existe, busca __len__; si tampoco existe, el objeto es truthy por defecto (cualquier instancia de clase personalizada sin estos métodos se considera verdadera).

Los objetos falsy en Python son exactamente estos:

ValorTipo
NoneNoneType
Falsebool
0, 0.0, 0jint, float, complex
"", b""str, bytes
[], (), {}, set()colecciones vacías
cualquier objeto cuyo __bool__ retorne False o __len__ retorne 0clase personalizada

Todo lo demás es truthy, incluyendo "0", [None], o 0.001.

Esto explica por qué if lista: es preferible a if len(lista) > 0:. No es solo cuestión de estilo: la primera forma respeta el protocolo de Python y funciona con cualquier colección que implemente __len__ o __bool__, incluyendo tipos que ni siquiera conoces en el momento de escribir el código. La segunda forma llama a len() explícitamente, lo que falla si el objeto no soporta len pero sí define __bool__. La preferencia idiomática tiene una razón de fondo.

El momento en que esto se vuelve crítico es con None. Un patrón muy común es retornar None para indicar ausencia de valor. Si simplemente haces if resultado: y resultado puede ser 0 o "" o [], estás confundiendo “no hay resultado” con “hay un resultado vacío/cero”. Ahí la distinción entre falsy y None concreto importa.

from __future__ import annotations
from dataclasses import dataclass


# Clase personalizada que implementa el protocolo de verdad
@dataclass
class ShoppingCart:
    items: list[str]
    discount: float = 0.0

    def __bool__(self) -> bool:
        # El carrito es "verdadero" solo si tiene ítems Y no tiene descuento completo
        return len(self.items) > 0 and self.discount < 1.0

    def __len__(self) -> int:
        return len(self.items)


def process_order(cart: ShoppingCart) -> str:
    # Usa el protocolo __bool__ directamente — no llama a len()
    if not cart:
        return "Carrito vacío o con descuento completo, nada que procesar."

    return f"Procesando {len(cart)} ítem(s) con {cart.discount * 100:.0f}% descuento."


def find_first_discount(carts: list[ShoppingCart]) -> ShoppingCart | None:
    for cart in carts:
        if cart.discount > 0:
            return cart
    return None  # Ausencia real de resultado


def report(cart: ShoppingCart | None) -> str:
    # Aquí la diferencia entre None y falsy importa
    if cart is None:
        return "No se encontró ningún carrito con descuento."

    # cart puede ser falsy (vacío) pero aun así existir como objeto
    if not cart:
        return f"Carrito con descuento encontrado pero está vacío o tiene 100% off."

    return f"Carrito con descuento del {cart.discount * 100:.0f}%: {cart.items}"


# --- Demo ---
carts = [
    ShoppingCart(["manzana", "pera"], discount=0.1),
    ShoppingCart([], discount=0.5),          # falsy: vacío
    ShoppingCart(["libro"], discount=1.0),   # falsy: descuento completo
    ShoppingCart(["café"]),                  # truthy
]

for c in carts:
    print(process_order(c))

print()
found = find_first_discount(carts)
print(report(found))

# Comparación directa: idiomático vs explícito
nombres = ["Ana", "Luis"]
print(bool(nombres))           # True — tiene elementos
print(bool([]))                # False — lista vacía
print(bool(ShoppingCart([])))  # False — __bool__ retorna False

Qué está pasando aquí

ShoppingCart define __bool__ con una lógica compuesta: un carrito con items pero con descuento del 100% se considera “falso” desde el punto de vista del procesamiento. Eso es legítimo y es exactamente para lo que sirve el protocolo. Cuando process_order escribe if not cart:, no le importa si el carrito está vacío o si tiene descuento completo — ambos casos son “no procesable”.

Fíjate en find_first_discount: retorna None explícitamente para indicar ausencia de resultado. En report, primero verificamos cart is None con identidad (is), no con igualdad booleana. Esto es clave: un carrito que existe pero es falsy no es lo mismo que la ausencia de carrito. Si usáramos if not cart: para el primer check, un carrito vacío con descuento sería incorrectamente reportado como “no encontrado”.

__len__ existe independientemente de __bool__. Cuando defines ambos, Python prioriza __bool__. Si solo defines __len__, Python infiere truthiness desde ahí: len == 0 es falsy. Puedes ver esto con cualquier lista: Python no tiene un __bool__ en list, solo tiene __len__.

Errores que debes conocer

Error: usar if x: cuando x puede ser 0 o "" con significado legítimo, confundiendo un valor válido falsy con ausencia de valor.

# ❌ Wrong
def get_score(data: dict, key: str) -> int | None:
    score = data.get(key)
    if score:  # 0 es un score válido, pero pasa como "no encontrado"
        return score
    return None

# ✅ Right
def get_score(data: dict, key: str) -> int | None:
    score = data.get(key)
    if score is not None:  # distingue explícitamente entre 0 y ausencia
        return score
    return None

data.get(key) retorna None cuando la clave no existe y 0 cuando el valor es cero. Solo is not None separa esos dos casos correctamente.


Error: usar if array: con arrays de NumPy o Series de pandas, donde el comportamiento rompe el protocolo estándar.

import numpy as np

# ❌ Wrong
arr = np.array([1, 2, 3])
if arr:  # lanza ValueError: ambiguous truth value
    print("tiene datos")

# ✅ Right
if arr.size > 0:          # para saber si tiene elementos
    print("tiene datos")

if arr.any():             # si algún elemento es truthy
    print("alguno es verdadero")

if arr.all():             # si todos los elementos son truthy
    print("todos son verdaderos")

NumPy sobreescribe __bool__ para lanzar ValueError cuando el array tiene más de un elemento, porque la pregunta “¿es verdadero este array?” es ambigua: ¿todos los elementos? ¿alguno? Pandas hace lo mismo con Series. Aquí el protocolo estándar de Python simplemente no aplica, y necesitas ser explícito sobre qué pregunta estás haciendo.

28

Dejar un comentario

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

Scroll al inicio