`bool` como subtipo de `int`: consecuencias reales

En Python, bool no es un tipo independiente que “se parece” a un número: es literalmente una subclase de int. Eso significa que True y False no son solo valores de verdad — son los enteros 1 y 0 con un nombre más expresivo. No es un detalle de implementación ni una coincidencia: es parte de la especificación del lenguaje.

>>> isinstance(True, int)
True
>>> type(True)
<class 'bool'>
>>> True == 1
True
>>> False == 0
True

¿Por qué funciona así? La decisión de diseño viene de Python 2.3, cuando se añadió bool al lenguaje. En lugar de crear un tipo completamente nuevo y romper código existente que ya usaba 0 y 1 como valores de verdad, se optó por hacer que bool heredara de int. Así la compatibilidad hacia atrás quedaba garantizada y el comportamiento era predecible. True y False son objetos singleton de tipo bool, pero el sistema de tipos de Python los reconoce como enteros en cualquier contexto donde se espere un int.

La jerarquía real es bool → int → object. Cuando Python evalúa True + True, no hay ninguna magia: simplemente suma 1 + 1 usando la aritmética de int. El resultado es 2, de tipo int — no bool.

Esto importa en producción porque hay un patrón idiomático que depende exactamente de este comportamiento: contar cuántos elementos de una colección cumplen una condición. Y también hay un conjunto de bugs silenciosos esperando a quien no lo sabe.

Ejemplo completo

from typing import Sequence


def analyze_temperatures(readings: Sequence[float]) -> dict:
    above_zero = sum(x > 0 for x in readings)
    below_zero = sum(x < 0 for x in readings)
    exactly_zero = sum(x == 0 for x in readings)

    # bool actúa como int en índices también
    flags = [True, False, True, True, False]
    active_count = sum(flags)  # suma directa de una lista de bools

    # isinstance revela la herencia
    for val in [True, False, 42]:
        print(f"{val!r:>6}  | type: {type(val).__name__:<5} | isinstance(val, int): {isinstance(val, int)}")

    # Demostración aritmética explícita
    print(f"\nTrue + True     = {True + True}")
    print(f"True + True + 1 = {True + True + 1}")
    print(f"type(True + True) = {type(True + True)}")  # int, no bool

    # El valor de retorno con bool como entero en diccionario
    severity = {0: "ok", 1: "warning", 2: "critical"}
    level = (above_zero == 0) + (below_zero > len(readings) // 2)
    # level es 0, 1 o 2 según las condiciones; se usa directamente como clave

    return {
        "total": len(readings),
        "above_zero": above_zero,
        "below_zero": below_zero,
        "exactly_zero": exactly_zero,
        "active_flags": active_count,
        "severity": severity.get(level, "unknown"),
    }


if __name__ == "__main__":
    data = [3.2, -1.1, 0.0, 5.5, -0.3, 0.0, 7.1]
    result = analyze_temperatures(data)
    for k, v in result.items():
        print(f"{k}: {v}")

Lo que está pasando aquí

sum(x > 0 for x in readings) es el corazón del ejemplo. La expresión generadora produce True o False para cada elemento, y sum() los acumula como si fueran 1 y 0. El resultado es exactamente el número de elementos que cumplen la condición — sin if, sin contador manual, sin len([x for x in readings if x > 0]). Es conciso porque el lenguaje garantiza que bool se comporta como int en contextos aritméticos.

Fíjate en type(True + True) — el resultado es int, no bool. Cuando bool participa en operaciones aritméticas, Python asciende al tipo base y devuelve int. El único momento en que obtienes bool es cuando el operador específicamente produce un valor de verdad (comparaciones, and, or, not).

La línea level = (above_zero == 0) + (below_zero > len(readings) // 2) muestra cómo combinar condiciones booleanas en un índice o nivel numérico. Cada comparación produce True/False, la suma da 0, 1 o 2, y eso se usa directamente como clave de diccionario. Esto es idiomático cuando las condiciones tienen peso uniforme y el resultado es un nivel ordinal.

El bloque con isinstance confirma algo que no es intuitivo a primera vista: isinstance(True, int) devuelve True, pero isinstance(42, bool) devuelve False. La herencia es unidireccional — todo bool es un int, pero no al revés.

Errores que debes conocer

Error: usar True/False como claves de diccionario junto con 1/0, esperando que sean entradas separadas, cuando en realidad son la misma clave.

# ❌ Wrong
d = {}
d[True] = "verdadero"
d[1] = "uno"
print(d)  # {True: "uno"} — solo hay UNA entrada, la segunda sobreescribió la primera
print(len(d))  # 1, no 2

# ✅ Right
d = {}
d["true"] = "verdadero"
d[1] = "uno"
print(d)  # {"true": "verdadero", 1: "uno"}
print(len(d))  # 2

True == 1 y hash(True) == hash(1), así que para los diccionarios y conjuntos son la misma clave. Usa strings u otros tipos si necesitas distinguirlos.


Error: comparar con == True en vez de evaluar directamente, lo que produce resultados inesperados cuando el valor es un entero.

# ❌ Wrong
def is_active(value):
    return value == True  # True solo si value es exactamente True o 1

print(is_active(2))   # False — aunque 2 es "truthy"
print(is_active("yes"))  # False — aunque es truthy

# ✅ Right
def is_active(value):
    return bool(value)  # convierte explícitamente, respeta la semántica de verdad

print(is_active(2))   # True
print(is_active("yes"))  # True

value == True compara aritméticamente (solo 1 y True son iguales a True); bool(value) evalúa la veracidad completa del objeto, que es lo que normalmente se quiere.


Error: asumir que isinstance(x, bool) es suficiente para filtrar booleanos fuera de una lista de enteros, olvidando que el orden de comprobación importa.

# ❌ Wrong
def process(value):
    if isinstance(value, int):   # True y False también entran aquí
        return value * 2
    return None

print(process(True))   # 2, no None — bool pasó el filtro de int

# ✅ Right
def process(value):
    if isinstance(value, bool):  # bool primero, porque es subclase de int
        return None
    if isinstance(value, int):
        return value * 2
    return None

print(process(True))   # None
print(process(5))      # 10

Hay que comprobar bool antes que int; de lo contrario, la comprobación de int captura también los booleanos por la herencia.


El patrón sum(condition for x in iterable) es una de esas herramientas que una vez que la ves, no puedes dejar de usarla — y ahora sabes exactamente por qué funciona.

22

Dejar un comentario

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

Scroll al inicio