domingo, 18 de enero de 2026

Principios Fundamentales de Diseño en Python - Parte 11 - Errores comunes que se debe evitar

Python Anti-Patterns: Errores Comunes a Evitar
Python // Anti-Patterns

Errores Comunes a Evitar

Prácticas de programación que degradan la calidad de tu código Python

Los anti-patterns son prácticas de programación comunes que, aunque no necesariamente incorrectas, frecuentemente conducen a código menos eficiente, menos legible y menos mantenible. Al comprender estas trampas, puedes escribir código más limpio y profesional.

En este artículo exploraremos cuatro categorías de anti-patterns: Violaciones de Estilo, Anti-patterns de Correctitud, Anti-patterns de Mantenibilidad y Anti-patterns de Rendimiento.

CATEGORIA_01

Violaciones de Estilo de Código

La guía de estilo de Python, conocida como PEP 8, proporciona recomendaciones para mejorar la legibilidad y consistencia del código. Seguir estas convenciones facilita la colaboración entre desarrolladores y el mantenimiento a largo plazo.

Herramientas de Formateo

Black
isort
Ruff
Flake8

Indentación

Debes usar cuatro espacios por nivel de indentación y evitar mezclar tabs con espacios.

✗ Incorrecto
def my_function():
  # 2 espacios - inconsistente
  if True:
      # 4 espacios - mezclado
      print("Bad")
✓ Correcto
def my_function():
    # 4 espacios consistentes
    if True:
        # 4 espacios más
        print("Good")

Longitud de Línea y Líneas en Blanco

La guía recomienda limitar todas las líneas de código a un máximo de 79 caracteres para mejor legibilidad. También existen reglas sobre líneas en blanco:

2 LÍNEAS
Entre definiciones de funciones y clases de nivel superior
1 LÍNEA
Entre métodos dentro de una clase
MODERADO
Para separar grupos lógicos de código

Imports

Los imports deben organizarse en grupos separados por líneas en blanco, en este orden:

imports_correctos.py
# 1. Imports de biblioteca estándar
import os
import sys

# 2. Imports de terceros
import requests
import numpy as np

# 3. Imports locales/aplicación
from myproject import utils
from myproject.models import User
Evita imports múltiples en una línea: import os, sys es incorrecto. Cada import debe estar en su propia línea.

Convenciones de Nombrado

naming_conventions.py
# Clases: CapWords (PascalCase)
class MyClassName:
    pass

# Funciones y variables: snake_case
def my_function_name():
    my_variable = 42
    return my_variable

# Constantes: SCREAMING_SNAKE_CASE
MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT = 30

# Atributos protegidos: _prefijo_underscore
class Example:
    def __init__(self):
        self._protected_attr = "internal"

Espacios en Expresiones

✗ Evitar
# Espacios dentro de paréntesis
spam( ham[ 1 ], { eggs: 2 } )

# Espacio antes de paréntesis
spam (1)

# Múltiples espacios alrededor de =
x             = 1
long_variable = 2
✓ Preferir
# Sin espacios dentro de paréntesis
spam(ham[1], {eggs: 2})

# Sin espacio antes de paréntesis
spam(1)

# Un espacio alrededor de =
x = 1
long_variable = 2
CATEGORIA_02

Anti-patterns de Correctitud

Estos anti-patterns pueden llevar a bugs sutiles y comportamientos inesperados en tu código. Son especialmente peligrosos porque el código puede parecer funcionar correctamente en casos simples.

Usar type() para Comparar Tipos

Usar type() para comparar tipos ignora la herencia y el polimorfismo, lo cual es fundamental en Python.

Problema con type()
type(obj) == SomeClass
Ignora subclases
✗ Anti-pattern
def process(obj):
    # No reconoce subclases
    if type(obj) == dict:
        return "Es un dict"
    return "No es un dict"

# defaultdict ES un dict, pero...
from collections import defaultdict
d = defaultdict(int)
print(process(d))  # "No es un dict" ¡ERROR!
✓ Solución
def process(obj):
    # isinstance() respeta herencia
    if isinstance(obj, dict):
        return "Es un dict"
    return "No es un dict"

from collections import defaultdict
d = defaultdict(int)
print(process(d))  # "Es un dict" ✓

Argumento Mutable por Defecto

Este es uno de los bugs más comunes en Python. Los argumentos mutables por defecto se evalúan una sola vez cuando se define la función, no cada vez que se llama.

Los objetos mutables como listas y diccionarios usados como valores por defecto se comparten entre todas las llamadas a la función.
mutable_default_bad.py
def add_item(item, items=[]):  # ⚠️ PELIGRO
    items.append(item)
    return items

# Primera llamada
print(add_item("a"))  # ['a'] - Parece correcto

# Segunda llamada - ¡SORPRESA!
print(add_item("b"))  # ['a', 'b'] - ¡La lista persiste!

# Tercera llamada
print(add_item("c"))  # ['a', 'b', 'c'] - Sigue acumulando
mutable_default_good.py
def add_item(item, items=None):  # ✓ Correcto
    if items is None:
        items = []  # Nueva lista cada vez
    items.append(item)
    return items

print(add_item("a"))  # ['a']
print(add_item("b"))  # ['b'] - Lista fresca
print(add_item("c"))  # ['c'] - Lista fresca

Acceder a Miembros Protegidos desde Fuera

Acceder a atributos con prefijo underscore desde fuera de la clase rompe la encapsulación y hace el código frágil ante cambios internos.

protected_members.py
class Book:
    def __init__(self, title, author):
        self._title = title    # Protegido
        self._author = author  # Protegido
    
    @property
    def title(self):
        return self._title
    
    @property
    def author(self):
        return self._author

book = Book("1984", "Orwell")

# ✗ No hacer esto
# print(book._title)  # Acceso directo a protegido

# ✓ Usar la propiedad pública
print(book.title)   # "1984"
print(book.author)  # "Orwell"
CATEGORIA_03

Anti-patterns de Mantenibilidad

Estos anti-patterns hacen que tu código sea difícil de entender y mantener a lo largo del tiempo. Evitarlos mejora significativamente la calidad de tu base de código.

Imports con Wildcard

Usar from module import * contamina el namespace y hace difícil determinar de dónde viene cada función o variable.

✗ Wildcard Import
from math import *
from statistics import *

# ¿De dónde viene sqrt?
# ¿Hay conflictos de nombres?
result = sqrt(16)
✓ Imports Específicos
from math import sqrt, pi
from statistics import mean

# Claro de dónde viene cada cosa
result = sqrt(16)
average = mean([1, 2, 3])

LBYL vs EAFP

LBYL (Look Before You Leap) verifica condiciones antes de actuar. EAFP (Easier to Ask Forgiveness than Permission) intenta la acción y maneja excepciones. Python favorece EAFP.

Comparación de Enfoques
LBYL: if → then
vs
EAFP: try → except
lbyl_approach.py
import os

# LBYL - Look Before You Leap
def read_file_lbyl(filename):
    if os.path.exists(filename):
        with open(filename) as f:
            return f.read()
    else:
        print("Archivo no encontrado")
        return None
eafp_approach.py
# EAFP - Easier to Ask Forgiveness than Permission
# Más Pythonic y evita condiciones de carrera
def read_file_eafp(filename):
    try:
        with open(filename) as f:
            return f.read()
    except FileNotFoundError:
        print("Archivo no encontrado")
        return None
EAFP evita condiciones de carrera (race conditions). Con LBYL, el archivo podría ser eliminado entre la verificación y la apertura.

Sobreuso de Herencia

Crear jerarquías de herencia profundas aumenta la complejidad y el acoplamiento. Favorece la composición sobre la herencia.

Jerarquía Problemática
GrandParent
Parent
Child
✗ Herencia Profunda
class GrandParent:
    pass

class Parent(GrandParent):
    pass

class Child(Parent):
    pass
    
# Alto acoplamiento
# Difícil de modificar
✓ Composición
class Parent:
    pass

class Child:
    def __init__(self, parent):
        self.parent = parent
        
# Bajo acoplamiento
# Flexible y testeable

Variables Globales para Compartir Datos

Las variables globales son accesibles en todo el programa, pero llevan a bugs donde diferentes partes modifican el estado inesperadamente. También causan problemas en entornos multithreading.

global_variable_bad.py
# ✗ Variable global - problemático
counter = 0

def increment():
    global counter
    counter += 1

def reset():
    global counter
    counter = 0

# Cualquier parte del código puede modificar counter
# Difícil de rastrear cambios
# Problemas en multithreading
encapsulated_state.py
# ✓ Estado encapsulado en clase
class Counter:
    def __init__(self):
        self.value = 0
    
    def increment(self):
        self.value += 1
    
    def reset(self):
        self.value = 0

# Uso
c = Counter()
c.increment()
print(c.value)  # 1
c.reset()
print(c.value)  # 0
python encapsulated_state.py
1
0
CATEGORIA_04

Anti-patterns de Rendimiento

Estos anti-patterns llevan a ineficiencias que pueden degradar el rendimiento, especialmente notables en aplicaciones de gran escala o tareas intensivas en datos.

Concatenar Strings en un Loop

Concatenar strings con + o += en un loop crea un nuevo objeto string cada vez, lo cual es altamente ineficiente. Cada concatenación tiene complejidad O(n).

Complejidad de Concatenación
+= en loop: O(n²)
vs
.join(): O(n)
string_concat_comparison.py
# ✗ Ineficiente - O(n²)
def concatenate_bad(items):
    result = ""
    for item in items:
        result += item  # Crea nuevo string cada vez
    return result

# ✓ Eficiente - O(n)
def concatenate_good(items):
    return "".join(items)  # Una sola operación

# Test con lista grande
words = ["word"] * 10000

import timeit
bad_time = timeit.timeit(lambda: concatenate_bad(words), number=100)
good_time = timeit.timeit(lambda: concatenate_good(words), number=100)

print(f"Concatenación +=: {bad_time:.4f}s")
print(f"Usando .join(): {good_time:.4f}s")
python string_concat_comparison.py
Concatenación +=: 0.8234s
Usando .join(): 0.0156s

Variables Globales para Caching

Usar variables globales para almacenar caché es problemático: no es thread-safe, no tiene control de tamaño, y contamina el namespace global.

global_cache_bad.py
import random
import time

# ✗ Caché global - problemático
_cache = {}

def get_data(user_id):
    if user_id not in _cache:
        # Simular operación costosa
        time.sleep(random.uniform(0.5, 1.0))
        _cache[user_id] = {"name": f"User_{user_id}"}
    return _cache[user_id]

# Problemas:
# - No es thread-safe
# - Sin límite de tamaño
# - Sin política de expiración
lru_cache_good.py
import random
import time
from functools import lru_cache

@lru_cache(maxsize=100)
def get_data(user_id):
    return _perform_expensive_operation(user_id)

def _perform_expensive_operation(user_id):
    time.sleep(random.uniform(0.5, 1.0))
    user_data = {
        1: {"name": "Alice", "email": "alice@example.com"},
        2: {"name": "Bob", "email": "bob@example.com"},
        3: {"name": "Charlie", "email": "charlie@example.com"},
    }
    return user_data.get(user_id, {"error": "User not found"})

# Uso
print(get_data(1))  # Lento - primera vez
print(get_data(1))  # Rápido - desde caché
print(get_data(2))  # Lento - nuevo usuario
print(get_data(99)) # Lento - usuario no existe
python lru_cache_good.py
{'name': 'Alice', 'email': 'alice@example.com'}
{'name': 'Alice', 'email': 'alice@example.com'}
{'name': 'Bob', 'email': 'bob@example.com'}
{'error': 'User not found'}
THREAD-SAFE
lru_cache maneja concurrencia automáticamente
TAMAÑO LIMITADO
maxsize controla el uso de memoria
LRU EVICTION
Elimina automáticamente entradas menos usadas
El decorador @lru_cache proporciona un caché LRU (Least Recently Used) que automáticamente gestiona el tamaño y la vida útil de las entradas del caché.

Resumen de Anti-Patterns

01 // Estilo de Código

Sigue PEP 8: indentación consistente, imports organizados, nombres descriptivos. Usa herramientas como Black y Ruff.

02 // Correctitud

Usa isinstance() sobre type(), evita argumentos mutables por defecto, respeta la encapsulación.

03 // Mantenibilidad

Evita wildcard imports, favorece EAFP sobre LBYL, usa composición sobre herencia profunda.

04 // Rendimiento

Usa .join() para concatenar strings, emplea @lru_cache en lugar de variables globales para caching.

Recuerda: El mejor código no es solo hacer que funcione, sino hacer que funcione bien. Idealmente, debe ser fácil de mantener.

Basado en "Mastering Python Design Patterns" // Capítulo 11: Python Anti-Patterns

Kamon Ayeva & Sakis Kasampalis