DevTools

Cheatsheet Python

Linguagem de programação versátil e fácil de aprender

Volver a los lenguajes
Python
109 tarjetas encontradas
Categorías:
Versiones:

Sintaxe Básica


12 cards
Variables y Asignación
nombre = "Ana"
edad = 30
precio = 9.99
activo = True

# Asignación múltiple
x, y, z = 1, 2, 3
a = b = c = 0

# Swap elegante
x, y = y, x

# Type hints (opcional)
nombre: str = "Ana"
edad: int = 30

Python tiene tipado dinámico: no es necesario declarar el tipo. Asignación múltiple con comas. x, y = y, x hace swap sin variable temporal. Los type hints son opcionales y no se fuerzan en runtime.

Operadores
# Aritméticos
10 + 3   # 13
10 - 3   # 7
10 * 3   # 30
10 / 3   # 3.333 (float)
10 // 3  # 3 (división entera)
10 % 3   # 1 (resto)
2 ** 10  # 1024 (potencia)

# Comparación
5 == 5   # True
5 != 3   # True
5 > 3    # True

# Lógicos
True and False  # False
True or False   # True
not True        # False

# Identidad y pertenencia
x is None       # True/False
5 in [1, 2, 5]  # True

/ devuelve siempre float; // hace división entera. ** es potencia. is compara identidad (mismo objeto), == compara valor. in verifica pertenencia.

Comentarios y Documentación
# Comentario de línea

"""
Docstring multi-línea
para módulos, clases y funciones.
"""

def suma(a: int, b: int) -> int:
    """Suma dos numeros.

    Args:
        a: Primer numero
        b: Segundo numero

    Returns:
        La suma de a y b
    """
    return a + b

# Acceder al docstring
help(suma)
print(suma.__doc__)

# para comentarios. """...""" (docstring) documenta funciones/clases. Convención: usar el formato Google style o NumPy style. help() muestra el docstring. Herramientas como Sphinx generan docs automáticos.

Tipos de Datos
# Tipos primitivos
entero = 10        # int
decimal = 3.14      # float
complexo = 2 + 3j   # complex
texto = "Hola"       # str
activo = True        # bool
vacio = None        # NoneType

# Colecciones
lista = [1, 2, 3]   # list
tuplo = (1, 2, 3)   # tuple
dicio = {"a": 1}    # dict
conj  = {1, 2, 3}   # set

# Verificar tipo
type(42)            # <class 'int'>
isinstance(42, int) # True

Tipos integrados: int, float, str, bool, NoneType. Colecciones: list, tuple, dict, set. Usa type() o isinstance() para verificar.

Conversiones (Casting)
int("42")       # 42
float("3.14")   # 3.14
str(100)        # "100"
bool(0)         # False
bool("texto")   # True

list("abc")     # ['a', 'b', 'c']
tuple([1, 2])   # (1, 2)
set([1, 1, 2])  # {1, 2}
dict([("a", 1)])  # {"a": 1}

int(3.9)        # 3 (¡trunca!)
round(3.9)      # 4
round(3.14159, 2)  # 3.14

Conversiones: int(), float(), str(), bool(). int() trunca decimales (no redondea). round() redondea. Valores falsy: 0, "", [], None, False.

Operadores de Identidad y Pertenencia
# is / is not (identidad)
a = [1, 2, 3]
b = [1, 2, 3]
a == b      # True (mismo valor)
a is b      # False (objetos diferentes)

c = a
c is a      # True (mismo objeto)

# in / not in (pertenencia)
5 in [1, 2, 5]        # True
"x" not in "texto"    # False
"a" in {"a": 1}       # True (clave)
3 in (1, 2, 3)        # True

# None: siempre con is
if valor is None:
    pass
if valor is not None:
    pass

is verifica si son el mismo objeto (identidad). == compara valores. Usa is None para None. in/not in verifica pertenencia en listas, dicts, strings, tuples y sets.

Strings — Métodos
s = "  Hola Mundo  "

s.strip()            # "Hola Mundo"
s.upper()            # "  HOLA MUNDO  "
s.lower()            # "  hola mundo  "
s.title()            # "  Hola Mundo  "
s.split()            # ["Hola", "Mundo"]
s.replace("Mundo", "Python")
s.startswith("Hola")  # False (tiene espacios)
s.find("Mundo")      # 6 (índice)
s.count("o")         # 2
len(s)               # 13

# Concatenación y repetición
"Hola" + " " + "Mundo"
"ha" * 3             # "hahaha"

Las strings son inmutables: los métodos devuelven una nueva string. strip() quita espacios. split() divide en lista. find() retorna el índice (-1 si no lo encuentra). len() da la longitud.

Entrada y Salida
# Salida
print("Hola", "Mundo")         # Hola Mundo
print("a", "b", sep="-")      # a-b
print("sem salto", end=" ")  # sin \n
print(f"Nombre: {nombre}")

# Entrada
nombre = input("Nombre: ")
edad = int(input("Edad: "))

# print con lista
print([1, 2, 3])       # [1, 2, 3]
print(*[1, 2, 3])      # 1 2 3 (unpack)
print(*[1, 2, 3], sep=", ")  # 1, 2, 3

print() con sep y end personaliza la salida. input() retorna siempre str — usa int() para convertir. *lista desempaqueta como argumentos separados.

Multi-línea y Expresiones
# String multi-línea
texto = """
Primera linea
Segunda linea
"""

# Expresión en varias líneas
resultado = (
    valor1
    + valor2
    + valor3
)

# Lista en varias líneas
frutas = [
    "manzana",
    "banana",
    "cereza",
]

# Backslash (evitar)
total = precio * \
        cantidad

"""...""" crea strings multi-línea. Los paréntesis () permiten expresiones en varias líneas (preferido). La coma final en listas es válida (trailing comma). Evita \ para continuación.

f-strings y Formateo
nombre = "Ana"
edad = 30
precio = 9.5

# f-string (recomendado)
print(f"A {nombre} tem {edad} anios")
print(f"Expresión: {edad + 5}")
print(f"Método: {nombre.upper()}")

# Formateo numérico
print(f"Precio: {precio:.2f}€")   # 9.50€
print(f"{0.85:.0%}")            # 85%
print(f"{1234567:,}")           # 1,234,567
print(f"{42:08d}")              # 00000042
print(f"{'centro':^20}")        # centrado

# Alternativas
"Hola {}".format(nombre)
"Hola %s" % nombre

Las f-strings (prefijo f) insertan expresiones en {}. Formateo: :.2f decimales, :, miles, :08d padding. .format() y % son alternativas antiguas.

None y Truthiness
# None es el "nulo" de Python
resultado = None
if resultado is None:
    print("Sem resultado")

# Truthy vs Falsy
# Falsy: False, 0, 0.0, "", [], {}, (), set(), None
# Truthy: todo lo demás

if []:       # False (lista vacía)
    pass
if [1, 2]:   # True (lista con items)
    pass

# Patrón: valor or default
nombre = input() or "Anónimo"
valor = datos.get("x") or 0

None es el valor nulo — compáralo con is None, nunca == None. Valores falsy: 0, "", [], {}, None. or retorna el primer valor truthy.

Constantes y Convenciones
# Python no tiene constantes reales
# Convención: UPPER_SNAKE_CASE
MAX_RETRIES = 3
API_URL = "https://api.ejemplo.com"
PI = 3.14159

# typing.Final (3.8+) — hint para IDEs
from typing import Final
VERSION: Final = "2.0.0"
TIMEOUT: Final[int] = 30

# Enum para constantes relacionadas
from enum import Enum, auto

class Status(Enum):
    ACTIVO = auto()
    INACTIVO = auto()
    SUSPENDIDO = auto()

Status.ACTIVO.value  # 1
Status.ACTIVO.name   # "ACTIVO"

Python no tiene constantes reales — usa UPPER_SNAKE_CASE por convención. typing.Final indica a la IDE que no debe cambiarse. Enum crea conjuntos de constantes relacionadas. auto() asigna valores automáticos.

Controlo de Fluxo


8 cards
if / elif / else
edad = 20

if edad >= 18:
    print("Adulto")
elif edad >= 13:
    print("Adolescente")
else:
    print("Niño")

# Sin llaves — ¡usa indentación!
# Operadores: and, or, not
if edad >= 18 and edad < 65:
    print("Edad activa")

# Condición inline
status = "adulto" if edad >= 18 else "menor"

Python usa indentación (4 espacios) en vez de llaves. elif = else if. Operadores lógicos: and, or, not. Expresión ternaria: x if cond else y.

break, continue y else
# break: sale del loop
for i in range(10):
    if i == 5:
        break
    print(i)  # 0, 1, 2, 3, 4

# continue: salta la iteración
for i in range(10):
    if i % 2 == 0:
        continue
    print(i)  # 1, 3, 5, 7, 9

# else en el loop (se ejecuta sin break)
for n in range(2, 20):
    for x in range(2, n):
        if n % x == 0:
            break
    else:
        print(f"{n} es primo")

break termina el loop. continue salta a la próxima iteración. else en loops se ejecuta solo si el loop termina sin break — útil para búsquedas.

Operador Ternario
# Sintaxis: valor_si_true if condición else valor_si_false
edad = 20
status = "adulto" if edad >= 18 else "menor"

# Con expresiones
resultado = x * 2 if x > 0 else 0

# Encadenado (¡evitar!)
nivel = "alto" if x > 100 else "medio" if x > 50 else "baixo"

# Con or (default)
nombre = input() or "Anónimo"
valor = datos.get("clave") or "default"

# En list comprehension
pares = [x for x in range(10) if x % 2 == 0]

Expresión ternaria: valor if condición else default. Más legible que if/else para casos simples. or da default para valores falsy. En comprehensions, el if va al final como filtro.

match / case (3.10+)
# Pattern matching (Python 3.10+)
def procesar(comando):
    match comando.split():
        case ["salir"]:
            return "Adiós"
        case ["hola", nombre]:
            return f"Hola, {nombre}!"
        case ["suma", *nums]:
            return sum(int(n) for n in nums)
        case _:
            return "Comando desconocido"

# Con tipos
match valor:
    case int():
        print("Es entero")
    case str():
        print("Es string")
    case [x, y]:
        print(f"Par: {x}, {y}")
    case _:
        print("Outro")

match/case (Python 3.10+) es pattern matching estructural. case _ es el default. Puede hacer unpack de listas, capturar variables y verificar tipos. Más potente que switch de otros lenguajes.

for e Iteración
# Iterar lista
for fruta in ["manzana", "banana", "cereza"]:
    print(fruta)

# range
for i in range(5):       # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 10, 3):  # 2, 5, 8
    print(i)

# Iterar string
for char in "Python":
    print(char)

# Iterar dict
for clave, valor in {"a": 1, "b": 2}.items():
    print(f"{clave}: {valor}")

# for con else (se ejecuta si no hay break)
for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            break
    else:
        print(f"{n} es primo")

for itera cualquier iterable. range(inicio, fin, paso) genera una secuencia. .items() itera clave+valor de un dict. for...else: el else se ejecuta si el loop termina sin break.

Comparaciones Encadenadas
# Python permite encadenar comparaciones
x = 15
0 < x < 100        # True (equivale a x > 0 and x < 100)
1 <= x <= 20       # True

# Múltiples
a, b, c = 1, 5, 10
a < b < c          # True
a < b > c          # False

# Con variables
limite_inf = 0
limite_sup = 100
if limite_inf <= x <= limite_sup:
    print("En el rango")

# any / all con condiciones
nums = [2, 4, 6, 8]
all(n % 2 == 0 for n in nums)  # True
any(n > 5 for n in nums)       # True

Python permite comparaciones encadenadas: 0 < x < 100 es más legible que x > 0 and x < 100. all() retorna True si todos cumplen. any() retorna True si alguno cumple.

while
# Loop con condición
contador = 0
while contador < 5:
    print(contador)
    contador += 1

# Loop infinito con break
while True:
    cmd = input("> ")
    if cmd == "salir":
        break
    procesar(cmd)

# while con else
n = 10
while n > 0:
    if n == 5:
        break
    n -= 1
else:
    print("Terminou sem break")

while repite mientras la condición sea True. while True + break para loops controlados por input. while...else: el else se ejecuta si no hay break. ¡Cuidado con los loops infinitos!

Loops Anidados
# Tabla de multiplicar
for i in range(1, 11):
    for j in range(1, 11):
        print(f"{i*j:4}", end="")
    print()

# Matriz
matriz = [[1, 2], [3, 4], [5, 6]]
for linea in matriz:
    for elem in linea:
        print(elem, end=" ")
    print()

# Aplanamiento con comprehension
plano = [x for sub in matriz for x in sub]
# [1, 2, 3, 4, 5, 6]

# enumerate anidado
for i, linea in enumerate(matriz):
    for j, val in enumerate(linea):
        print(f"[{i}][{j}] = {val}")

Loops anidados para matrices y tablas. En comprehensions: el for externo va primero. enumerate() da índice + valor. Para aplanar: [x for sub in lista for x in sub].

Funções


10 cards
Definir Funciones
def saludo(nombre: str) -> str:
    """Retorna un saludo."""
    return f"Hola, {nombre}!"

# Valores por defecto
def potencia(base, exponente=2):
    return base ** exponente

potencia(3)       # 9
potencia(2, 10)   # 1024

# Múltiples retornos (tuple)
def dividir(a, b):
    return a // b, a % b

quoc, resto = dividir(17, 5)  # 3, 2

Funciones con def. Type hints opcionales (: str, -> str). Valores default en los parámetros. Retorno múltiple vía tuple con unpack. Sin return, retorna None.

Closures y Scope
# Closure: función que "recuerda" el scope
def contador():
    count = 0
    def incrementar():
        nonlocal count
        count += 1
        return count
    return incrementar

c = contador()
c()  # 1
c()  # 2
c()  # 3

# LEGB: Local, Enclosing, Global, Built-in
x = "global"
def outer():
    x = "enclosing"
    def inner():
        x = "local"
        print(x)
    inner()

# global
def set_global():
    global x
    x = "modificado"

Closure: función interna que captura variables del scope externo. nonlocal permite modificar la variable del scope envolvente. Regla LEGB: Local → Enclosing → Global → Built-in. global accede a la variable global.

Funciones como Objetos
# Las funciones son first-class
def gritar(texto):
    return texto.upper()

def sussurrar(texto):
    return texto.lower()

# Pasar como argumento
def execute(fn, msg):
    return fn(msg)

execute(gritar, "hola")     # "HOLA"
execute(sussurrar, "HOLA")  # "hola"

# Guardar en estructuras
ops = {
    "suma": lambda a, b: a + b,
    "mult": lambda a, b: a * b,
}
ops["suma"](3, 4)  # 7

# Retornar función
def multiplicador(n):
    return lambda x: x * n

doble = multiplicador(2)
doble(5)  # 10

Las funciones son objetos first-class: pueden pasarse, guardarse y retornarse. Las higher-order functions reciben/retornan funciones. Base de decorators, callbacks y patrones como Strategy.

*args y **kwargs
# *args: argumentos posicionales variables
def suma(*nums):
    return sum(nums)

suma(1, 2, 3)      # 6
suma(1, 2, 3, 4)   # 10

# **kwargs: argumentos nombrados variables
def perfil(**datos):
    for clave, valor in datos.items():
        print(f"{clave}: {valor}")

perfil(nombre="Ana", edad=30)

# Combinar
def func(*args, **kwargs):
    print(args)    # tuple
    print(kwargs)  # dict

# Unpacking
lista = [1, 2, 3]
suma(*lista)  # 6

*args recibe argumentos extras como tuple. **kwargs recibe nombrados como dict. Orden: parámetros normales, *args, keyword-only, **kwargs. *lista desempaqueta.

Type Hints
from typing import Optional, Union, List, Dict

def saudar(nombre: str) -> str:
    return f"Hola, {nombre}"

def buscar(id: int) -> Optional[str]:
    ...  # retorna str o None

def procesar(x: int | str) -> None:
    ...  # Python 3.10+: union con |

def total(nums: list[int]) -> float:
    return sum(nums)

def config() -> dict[str, int]:
    return {"timeout": 30}

# Callable
from typing import Callable
def aplicar(fn: Callable[[int], str], x: int) -> str:
    return fn(x)

Los type hints no se fuerzan en runtime — son para IDEs y mypy. Optional[X] = X | None. Python 3.10+: int | str en vez de Union. list[int] en vez de List[int] (3.9+).

functools — Herramientas
from functools import lru_cache, partial, reduce, wraps

# lru_cache: memoization
@lru_cache(maxsize=128)
def fib(n):
    return n if n < 2 else fib(n-1) + fib(n-2)

# cache (3.9+): lru_cache sin límite
from functools import cache
@cache
def factorial(n):
    return n * factorial(n-1) if n else 1

# partial: fija argumentos
def potencia(base, exp):
    return base ** exp
cuadrado = partial(potencia, exp=2)
cuadrado(5)  # 25

# singledispatch: overload por tipo
from functools import singledispatch
@singledispatch
def procesar(x):
    return str(x)
@procesar.register(list)
def _(x):
    return ", ".join(map(str, x))

@lru_cache memoiza resultados. @cache (3.9+) es sin límite. partial() fija argumentos de una función. @singledispatch crea funciones genéricas con overload por tipo. @wraps preserva metadatos en decorators.

Lambda y Funciones Anónimas
# Lambda: función de una expresión
doble = lambda x: x * 2
doble(5)  # 10

suma = lambda a, b: a + b
suma(3, 4)  # 7

# Uso común: ordenación
personas = [("Ana", 30), ("Ray", 25), ("Eva", 35)]
personas.sort(key=lambda p: p[1])
# [("Ray", 25), ("Ana", 30), ("Eva", 35)]

# Con map/filter
nums = [1, 2, 3, 4, 5]
list(map(lambda x: x**2, nums))     # [1, 4, 9, 16, 25]
list(filter(lambda x: x > 3, nums)) # [4, 5]

# sorted con key
sorted(personas, key=lambda p: p[0])

lambda crea funciones anónimas de una expresión. Ideal para key en sort()/sorted() y callbacks cortos. Para lógica compleja, usa def normal.

Funciones Integradas Útiles
# Transformación
list(map(str, [1, 2, 3]))       # ['1', '2', '3']
list(filter(None, [0, 1, "", "a"]))  # [1, "a"]

# Agregación
sum([1, 2, 3])       # 6
min([3, 1, 2])       # 1
max([3, 1, 2])       # 3
len([1, 2, 3])       # 3

# Inspección
type(42)             # <class 'int'>
isinstance(42, int)  # True
dir(str)             # métodos de str
help(print)          # documentación

# Otras
sorted([3, 1, 2])          # [1, 2, 3]
reversed([1, 2, 3])        # iterator
abs(-5)                    # 5
divmod(17, 5)              # (3, 2)

Funciones integradas: map(), filter(), sum(), min(), max(), len(), sorted(), abs(), divmod(). isinstance() verifica el tipo. dir() lista atributos.

Decorators
import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        inicio = time.perf_counter()
        resultado = func(*args, **kwargs)
        fin = time.perf_counter()
        print(f"{func.__name__}: {fin-inicio:.4f}s")
        return resultado
    return wrapper

@timer
def proceso_lento():
    time.sleep(1)

proceso_lento()  # "proceso_lento: 1.0012s"

# Decorator con argumentos
def repetir(n):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

Los decorators envuelven funciones con @nombre. El wrapper añade comportamiento antes/después. @functools.wraps preserva los metadatos. Los decorators con argumentos necesitan 3 niveles. Usados en logging, cache, auth.

Recursión
# Factorial
def factorial(n: int) -> int:
    if n <= 1:
        return 1
    return n * factorial(n - 1)

factorial(5)  # 120

# Fibonacci
def fib(n: int) -> int:
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

# Con memoization
from functools import lru_cache

@lru_cache(maxsize=None)
def fib_cache(n: int) -> int:
    if n < 2:
        return n
    return fib_cache(n - 1) + fib_cache(n - 2)

fib_cache(100)  # instantáneo

Recursión: función que se llama a sí misma. Necesita un caso base para parar. @lru_cache memoiza resultados (evita recálculos). Python tiene límite de recursión (~1000) — para casos grandes, prefiere iteración.

Classes e OOP


10 cards
Clase Básica
class Persona:
    # Atributo de clase
    especie = "Homo sapiens"

    def __init__(self, nombre: str, edad: int):
        # Atributos de instancia
        self.nombre = nombre
        self.edad = edad

    def apresentar(self) -> str:
        return f"Soy {self.nombre}, {self.edad} anios"

    def __repr__(self):
        return f"Persona({self.nombre!r}, {self.edad})"

ana = Persona("Ana", 30)
ana.apresentar()  # "Soy Ana, 30 anios"
ana.nombre          # "Ana"

__init__ es el constructor. self es la instancia (obligatorio). Los atributos de clase se comparten; los de instancia son únicos. __repr__ define la representación. Creación: Persona("Ana", 30).

Dunder Methods
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __len__(self):
        return 2

    def __getitem__(self, i):
        return (self.x, self.y)[i]

v = Vector(1, 2) + Vector(3, 4)
print(v)      # Vector(4, 6)
v[0]          # 4

Los dunder methods (__x__) definen el comportamiento de los operadores. __add__ para +, __eq__ para ==, __repr__ para print, __len__ para len(), __getitem__ para indexación.

__slots__ y Rendimiento
class Ponto:
    __slots__ = ('x', 'y')

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Ponto(1, 2)
p.x = 10      # OK
p.z = 3       # AttributeError!

# Sin __slots__: cada instancia tiene __dict__
# Con __slots__: sin __dict__, menos memoria

# Comparación
import sys
class Normal:
    def __init__(self): self.x = 1

class Slim:
    __slots__ = ('x',)
    def __init__(self): self.x = 1

sys.getsizeof(Normal().__dict__)  # ~104 bytes
# Slim: sin __dict__

__slots__ impide crear atributos dinámicos y elimina el __dict__ por instancia. Reduce la memoria en ~40% para muchas instancias. No puedes añadir atributos no declarados. Ideal para clases con millones de instancias.

Herencia y Polimorfismo
class Animal:
    def __init__(self, nombre: str):
        self.nombre = nombre

    def hablar(self) -> str:
        raise NotImplementedError

class Perro(Animal):
    def hablar(self) -> str:
        return f"{self.nombre}: Au au!"

class Gato(Animal):
    def hablar(self) -> str:
        return f"{self.nombre}: Miau!"

# Polimorfismo
animales = [Perro("Rex"), Gato("Felix")]
for a in animales:
    print(a.hablar())

# Herencia múltiple
class AnimalDomestico(Animal, Mascote):
    pass

Herencia con class Filha(Pai). super() llama a los métodos del padre. Polimorfismo: mismo método, comportamiento diferente. Python soporta herencia múltiple con MRO (Method Resolution Order).

Dataclass (3.7+)
from dataclasses import dataclass, field

@dataclass
class Producto:
    nombre: str
    precio: float
    cantidad: int = 0
    tags: list[str] = field(default_factory=list)

    @property
    def total(self) -> float:
        return self.precio * self.cantidad

p1 = Producto("Libro", 19.99, 2)
p2 = Producto("Libro", 19.99, 2)

p1 == p2       # True (¡compara campos!)
print(p1)      # Producto(nombre='Libro', precio=19.99, ...)
p1.total       # 39.98

# Inmutable
@dataclass(frozen=True)
class Ponto:
    x: float
    y: float

@dataclass genera __init__, __repr__, __eq__ automáticamente. field(default_factory=list) para mutables. frozen=True lo hace inmutable. Ideal para DTOs y value objects.

Enums (3.4+)
from enum import Enum, IntEnum, auto, Flag

class Color(Enum):
    ROJO = 1
    VERDE = 2
    AZUL = 3

class Status(Enum):
    ACTIVO = auto()    # 1
    INACTIVO = auto()  # 2

# Backed enum (con valores)
class HttpMethod(Enum):
    GET = "GET"
    POST = "POST"
    PUT = "PUT"

# IntEnum: comparable con int
class Prioridad(IntEnum):
    BAJA = 1
    MEDIA = 2
    ALTA = 3

Prioridad.ALTA > Prioridad.BAJA  # True

# Flag: bitwise
class Perm(Flag):
    READ = auto()
    WRITE = auto()
    EXEC = auto()

Enum crea conjuntos de constantes nombradas. auto() asigna valores automáticos. IntEnum es comparable con enteros. Flag soporta operaciones bitwise (|, &). Accede con .value y .name.

super() y Herencia
class Vehículo:
    def __init__(self, marca: str, anio: int):
        self.marca = marca
        self.anio = anio

class Coche(Vehículo):
    def __init__(self, marca: str, anio: int, puertas: int):
        super().__init__(marca, anio)
        self.puertas = puertas

    def info(self) -> str:
        base = f"{self.marca} ({self.anio})"
        return f"{base} - {self.puertas} puertas"

# MRO
print(Coche.__mro__)
# (Coche, Vehículo, object)

super().__init__() llama al constructor de la clase padre. Esencial en herencia para inicializar los atributos del padre. __mro__ muestra el orden de resolución de métodos. En herencia múltiple, sigue el orden del MRO.

Métodos de Clase y Estáticos
class Data:
    def __init__(self, dia, mes, anio):
        self.dia = dia
        self.mes = mes
        self.anio = anio

    @classmethod
    def from_string(cls, s: str) -> "Data":
        dia, mes, anio = map(int, s.split("/"))
        return cls(dia, mes, anio)

    @staticmethod
    def e_valida(s: str) -> bool:
        try:
            d, m, a = map(int, s.split("/"))
            return 1 <= m <= 12 and 1 <= d <= 31
        except ValueError:
            return False

# Factory method
d = Data.from_string("25/12/2024")
Date.is_valid("31/02/2024")  # False

@classmethod recibe cls (la clase) — ideal para factory methods. @staticmethod no recibe self ni cls — es una función normal en la clase. Usa @classmethod para constructores alternativos.

Properties
class Círculo:
    def __init__(self, radio: float):
        self._radio = radio

    @property
    def radio(self) -> float:
        return self._radio

    @radio.setter
    def radio(self, valor: float):
        if valor < 0:
            raise ValueError("Radio no pode ser negativo")
        self._radio = valor

    @property
    def area(self) -> float:
        return 3.14159 * self._radio ** 2

c = Círculo(5)
c.radio        # 5 (getter)
c.radio = 10   # setter con validación
c.area        # 314.159 (read-only)

@property crea getters/setters con sintaxis de atributo. @x.setter para validación en la asignación. Sin setter = read-only. Convención: atributo interno con prefijo _. Acceso: obj.prop (sin paréntesis).

ABC y Protocol
from abc import ABC, abstractmethod

class Forma(ABC):
    @abstractmethod
    def area(self) -> float: ...

    @abstractmethod
    def perímetro(self) -> float: ...

class Círculo(Forma):
    def __init__(self, radio):
        self.radio = radio

    def area(self):
        return 3.14159 * self.radio ** 2

    def perímetro(self):
        return 2 * 3.14159 * self.radio

# Protocol (duck typing estático)
from typing import Protocol

class Renderizavel(Protocol):
    def render(self) -> str: ...

ABC + @abstractmethod fuerza la implementación en las subclases. No puedes instanciar un ABC directamente. Protocol (3.8+) define interfaces por duck typing estático — sin herencia explícita. Verificado por mypy.

Coleções


9 cards
Listas
frutas = ["manzana", "banana", "cereza"]

# Acceso y modificación
frutas[0]       # "manzana"
frutas[-1]      # "cereza" (último)
frutas[1] = "kiwi"

# Métodos
frutas.append("uva")       # añade al final
frutas.insert(1, "mango")  # inserta en el índice
frutas.remove("banana")    # elimina por valor
frutas.pop()               # elimina y retorna el último
frutas.extend(["a", "b"])  # extiende

# Verificación
"kiwi" in frutas    # True
len(frutas)         # longitud
frutas.index("kiwi")  # índice

# Copia
copia = frutas.copy()
copia = frutas[:]

Las listas son mutables y ordenadas. Los índices negativos cuentan desde el final. append() añade, pop() elimina. in verifica existencia. Copia con .copy() o [:] (copia superficial).

Slicing (Fatiamento)
nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

nums[2:5]     # [2, 3, 4]
nums[:3]      # [0, 1, 2]
nums[7:]      # [7, 8, 9]
nums[-3:]     # [7, 8, 9]
nums[::2]     # [0, 2, 4, 6, 8] (paso 2)
nums[::-1]    # [9, 8, ..., 0] (invertir)
nums[1:8:2]   # [1, 3, 5, 7]

# Strings
"Python"[1:4]   # "yth"
"Python"[::-1]  # "nohtyP"

# Asignación con slice
nums[2:5] = [20, 30, 40]

# Copia superficial
copia = nums[:]

Slicing: [inicio:fin:paso]. fin es exclusivo. [::-1] invierte. [::2] salta de 2 en 2. Funciona en listas, strings y tuples. La asignación con slice modifica la lista original.

Arrays y Memoria
import array
import sys

# array: tipo fijo, menos memoria
nums = array.array('i', [1, 2, 3, 4, 5])
nums.append(6)
nums[0]  # 1

# Tipos: 'i' int, 'f' float, 'd' double, 'b' byte
print(sys.getsizeof([1,2,3]))       # ~88 bytes (list)
print(sys.getsizeof(array.array('i', [1,2,3])))  # ~76 bytes

# bytes y bytearray
datos = b"\x48\x65\x6c\x6c\x6f"  # b"Hello"
mutable = bytearray(b"abc")
mutable[0] = 65  # b"Abc"

# memoryview (zero-copy)
view = memoryview(bytearray(1024))
view[0:4] = b"test"

# Para numeros: numpy
# import numpy as np
# arr = np.array([1, 2, 3])

array.array es una lista con tipo fijo — menos memoria. bytes es inmutable, bytearray es mutable. memoryview permite acceso zero-copy a buffers. Para computación numérica, usa numpy.

Diccionarios
persona = {"nombre": "Ana", "edad": 30}

# Acceso
persona["nombre"]           # "Ana"
persona.get("email")      # None (sin error)
persona.get("email", "N/A")  # "N/A"

# Modificación
persona["email"] = "ana@mail.com"
del persona["edad"]
persona.update({"ciudad": "Lisbon"})

# Iteración
for clave in persona:
    print(clave, persona[clave])
for clave, valor in persona.items():
    print(f"{clave}: {valor}")

# Verificación
"nombre" in persona    # True
len(persona)         # 2

# Dict comprehension
quadrados = {x: x**2 for x in range(5)}

Los Dicts mapean clave → valor. .get() evita KeyError. .items() itera pares. in verifica claves. Desde Python 3.7, los dicts mantienen el orden de inserción. Las claves deben ser hashable.

Ordenación
nums = [3, 1, 4, 1, 5, 9]

# sorted: retorna nueva lista
sorted(nums)              # [1, 1, 3, 4, 5, 9]
sorted(nums, reverse=True)  # [9, 5, 4, 3, 1, 1]

# .sort(): modifica in-place
nums.sort()

# Con key
palabras = ["banana", "higo", "piña"]
sorted(palabras, key=len)  # por longitud
sorted(palabras, key=str.lower)

# Lista de tuples
personas = [("Ana", 30), ("Ray", 25)]
sorted(personas, key=lambda p: p[1])

# operator.itemgetter
from operator import itemgetter
sorted(personas, key=itemgetter(1))

sorted() retorna una nueva lista; .sort() modifica in-place. key define el criterio. reverse=True para descendente. operator.itemgetter es más rápido que lambda para tuples.

Tuples y NamedTuple
# Tuple: inmutable
ponto = (3, 4)
x, y = ponto  # unpacking

# Sin paréntesis también funciona
coords = 10, 20, 30

# Tuple de 1 elemento
single = (42,)  # ¡coma obligatoria!

# NamedTuple (tuple con nombres)
from collections import namedtuple
Ponto = namedtuple("Ponto", ["x", "y"])
p = Ponto(3, 4)
p.x  # 3
p.y  # 4

# typing.NamedTuple (con type hints)
from typing import NamedTuple
class Ponto2(NamedTuple):
    x: float
    y: float

p2 = Ponto2(1.5, 2.5)

Los Tuples son inmutables — más rápidos que las listas. Unpacking: x, y = ponto. Un tuple de 1 elemento necesita coma: (42,). namedtuple agrega nombres a los campos. Ideal para retornar múltiples valores.

collections
from collections import Counter, defaultdict, deque

# Counter: cuenta ocurrencias
texto = "abracadabra"
c = Counter(texto)
c.most_common(2)  # [('a', 5), ('b', 2)]

# defaultdict: valor por defecto
grupos = defaultdict(list)
for nombre, grupo in [("Ana", "A"), ("Ray", "B"), ("Eva", "A")]:
    grupos[grupo].append(nombre)
# {'A': ['Ana', 'Eva'], 'B': ['Ray']}

# deque: fila con operaciones O(1) en los extremos
fila = deque([1, 2, 3])
fila.appendleft(0)  # [0, 1, 2, 3]
fila.popleft()      # 0 (O(1)!)
fila.rotate(1)      # rota elementos

Counter cuenta elementos y da most_common(). defaultdict crea valor automáticamente para claves nuevas. deque es una fila con appendleft()/popleft() en O(1) — más rápido que lista para filas.

Sets
# Set: colección sin duplicados
nums = {1, 2, 3, 3, 2}  # {1, 2, 3}

# Operaciones
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

a | b   # unión: {1, 2, 3, 4, 5, 6}
a & b   # intersección: {3, 4}
a - b   # diferencia: {1, 2}
a ^ b   # simétrica: {1, 2, 5, 6}

# Métodos
a.add(10)
a.discard(1)    # elimina sin error
a.issubset(b)   # False
a.issuperset(b) # False

# Eliminar duplicados de lista
lista = [1, 2, 2, 3, 3, 3]
unica = list(set(lista))  # [1, 2, 3]

# Set vacío
s = set()  # ¡{} crea un dict vacío!

Los Sets son colecciones sin duplicados y no ordenadas. Operaciones: | unión, & intersección, - diferencia, ^ simétrica. {} crea un dict vacío — usa set(). Ideal para membership testing (O(1)).

Empaquetado y Unpacking
# Unpacking básico
a, b, c = [1, 2, 3]

# Con * (resto)
primero, *medio, último = [1, 2, 3, 4, 5]
# primero=1, medio=[2,3,4], último=5

# Ignorar valores
_, segundo, _ = (10, 20, 30)

# Merge de dicts (3.9+)
d1 = {"a": 1}
d2 = {"b": 2}
merged = d1 | d2  # {"a": 1, "b": 2}

# Unpack en función
def suma(a, b, c):
    return a + b + c
suma(*[1, 2, 3])  # 6

# Unpack dict
def perfil(nombre, edad):
    pass
perfil(**{"nombre": "Ana", "edad": 30})

Unpacking: a, b, c = lista. *resto captura elementos restantes. _ ignora valores. d1 | d2 hace merge de dicts (3.9+). *args y **kwargs desempaqueta en llamadas de función.

Compreensões e Iteradores


7 cards
List Comprehension
# Básica
quadrados = [x**2 for x in range(10)]

# Con filtro
pares = [x for x in range(20) if x % 2 == 0]

# Con transformación
nombres = ["ana", "rui", "eva"]
maiusculos = [n.upper() for n in nombres]

# Anidada (aplanar matriz)
matriz = [[1, 2], [3, 4], [5, 6]]
plano = [x for sub in matriz for x in sub]
# [1, 2, 3, 4, 5, 6]

# Con condición inline
resultado = [x if x > 0 else 0 for x in nums]

# vs map/filter
[x**2 for x in nums if x > 3]
# equivale a: list(map(lambda x: x**2, filter(lambda x: x > 3, nums)))

Sintaxis: [expresión for item in iterable if condición]. Más legible que map()/filter(). Para anidadas: for externo primero. Evita comprehensions muy complejas — usa loop normal.

itertools
from itertools import (
    chain, product, combinations,
    permutations, groupby, islice, cycle
)

# chain: concatena iterables
list(chain([1, 2], [3, 4]))  # [1, 2, 3, 4]

# product: producto cartesiano
list(product("AB", "12"))
# [('A','1'), ('A','2'), ('B','1'), ('B','2')]

# combinations y permutations
list(combinations([1,2,3], 2))  # [(1,2),(1,3),(2,3)]
list(permutations([1,2,3], 2))  # 6 pares

# groupby (requiere ordenación)
datos = sorted(personas, key=lambda p: p[1])
for grupo, ítems in groupby(datos, key=lambda p: p[1]):
    print(grupo, list(ítems))

# islice: slice de un iterador
list(islice(range(100), 5, 10))

itertools tiene herramientas para iterables. chain() concatena. product() hace producto cartesiano. combinations()/permutations() para combinatoria. groupby() agrupa (requiere datos ordenados). Todo es lazy.

Dict y Set Comprehension
# Dict comprehension
quadrados = {x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# Invertir dict
original = {"a": 1, "b": 2, "c": 3}
invertido = {v: k for k, v in original.items()}

# Filtrar dict
notas = {"Ana": 15, "Ray": 8, "Eva": 12}
aprovados = {k: v for k, v in notas.items() if v >= 10}

# Set comprehension
unicos = {x % 5 for x in range(20)}
# {0, 1, 2, 3, 4}

# De lista de tuples
pares = [(1, "um"), (2, "dois")]
d = {k: v for k, v in pares}

Dict comprehension: {clave: valor for ...}. Set comprehension: {expresión for ...}. Ideal para transformar/invertir dicts y crear sets con lógica. Misma sintaxis que list comp pero con {}.

reduce y functools
from functools import reduce, lru_cache, partial

# reduce: acumula
nums = [1, 2, 3, 4, 5]
reduce(lambda acc, x: acc + x, nums)  # 15
reduce(lambda acc, x: acc * x, nums)  # 120

# Con valor inicial
reduce(lambda acc, x: acc + x, nums, 100)  # 115

# lru_cache: memoization
@lru_cache(maxsize=128)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

# partial: fija argumentos
from functools import partial
doble = partial(int, base=2)
doble("1010")  # 10

# accumulate
from itertools import accumulate
list(accumulate([1, 2, 3, 4]))  # [1, 3, 6, 10]

reduce() acumula valores (suma, producto, etc.). @lru_cache memoiza resultados de funciones puras. partial() fija argumentos de una función. accumulate() es como reduce pero retorna todos los pasos.

Generators
# Generator function
def conteo(n):
    i = 0
    while i < n:
        yield i
        i += 1

for x in conteo(5):
    print(x)  # 0, 1, 2, 3, 4

# Generator expression (lazy)
suma = sum(x**2 for x in range(1000000))
# ¡No crea lista en memoria!

# vs List comprehension
import sys
lista = [x for x in range(10000)]
gen = (x for x in range(10000))
sys.getsizeof(lista)  # ~87616 bytes
sys.getsizeof(gen)    # ~112 bytes

# yield from (delegar)
def chain(*iterables):
    for it in iterables:
        yield from it

yield crea generators: evaluación lazy (un valor a la vez). (x for x in ...) es una generator expression. Usa mucha menos memoria que las listas. yield from delega a otro iterable. Ideal para secuencias infinitas.

Iteradores Personalizados
# Protocolo de iteración: __iter__ + __next__
class ContagemRegressiva:
    def __init__(self, inicio):
        self.n = inicio

    def __iter__(self):
        return self

    def __next__(self):
        if self.n <= 0:
            raise StopIteration
        self.n -= 1
        return self.n + 1

for x in ContagemRegressiva(5):
    print(x)  # 5, 4, 3, 2, 1

# Más simple: generator
def conteo(n):
    while n > 0:
        yield n
        n -= 1

# iter() con callable
import random
rolar = iter(lambda: random.randint(1, 6), 6)
# Genera numeros hasta que salga 6

Los iteradores implementan __iter__() y __next__(). StopIteration termina la iteración. Los generators con yield son más simples. iter(callable, sentinel) llama hasta retornar el sentinel.

zip, enumerate y map
# zip: combina iterables
nombres = ["Ana", "Ray"]
edades = [30, 25]
for nombre, edad in zip(nombres, edades):
    print(f"{nombre}: {edad}")

# zip para crear dict
d = dict(zip(nombres, edades))

# enumerate: índice + valor
for i, fruta in enumerate(["manzana", "banana"]):
    print(f"{i}: {fruta}")

# map: aplica función a todos
list(map(str.upper, ["hola", "mundo"]))
list(map(lambda x: x*2, [1, 2, 3]))

# filter: mantiene los True
list(filter(lambda x: x > 2, [1, 2, 3, 4]))

# zip_longest (tamaños diferentes)
from itertools import zip_longest
list(zip_longest([1, 2], [3], fillvalue=0))

zip() combina iterables en pares. enumerate() da índice + valor. map() aplica una función a todos. filter() mantiene elementos truthy. zip_longest() para tamaños diferentes con fillvalue.

Exceções e Ficheiros


9 cards
try / except / finally
try:
    resultado = 10 / 0
except ZeroDivisionError as e:
    print(f"Error: {e}")
except (TypeError, ValueError) as e:
    print(f"Tipo ou valor: {e}")
except Exception as e:
    print(f"Genérico: {e}")
else:
    print("Sem errores!")
finally:
    print("Sempre executa")

# Múltiples excepciones
try:
    valor = int(input())
except ValueError:
    print("No es numero")
except KeyboardInterrupt:
    print("Cancelado")

try/except captura errores. as e da acceso a la excepción. else se ejecuta si no hay error. finally se ejecuta siempre (cleanup). Captura excepciones específicas antes de las genéricas.

JSON y CSV
import json
import csv

# JSON
datos = {"nombre": "Ana", "edad": 30}
json_str = json.dumps(datos, ensure_ascii=False, indent=2)
objeto = json.loads(json_str)

# JSON en archivo
with open("datos.json", "w") as f:
    json.dump(datos, f, ensure_ascii=False)
with open("datos.json") as f:
    datos = json.load(f)

# CSV
with open("datos.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["Nombre", "Edad"])
    writer.writerow(["Ana", 30])

with open("datos.csv") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["Nombre"])

json.dumps()/loads() para strings. json.dump()/load() para archivos. ensure_ascii=False para acentos. csv.DictReader lee con nombres de columna. csv.writer escribe líneas.

ExceptionGroup (3.11+)
# ExceptionGroup: múltiples excepciones (3.11+)
try:
    raise ExceptionGroup("errores", [
        ValueError("valor inválido"),
        TypeError("tipo errado"),
    ])
except* ValueError as eg:
    for e in eg.exceptions:
        print(f"ValueError: {e}")
except* TypeError as eg:
    for e in eg.exceptions:
        print(f"TypeError: {e}")

# TaskGroup (asyncio, 3.11+)
import asyncio

async def main():
    async with asyncio.TaskGroup() as tg:
        t1 = tg.create_task(fetch("url1"))
        t2 = tg.create_task(fetch("url2"))
    # Si una falla, todas se cancelan
    print(t1.result(), t2.result())

ExceptionGroup (3.11+) agrupa múltiples excepciones. except* captura por tipo dentro del grupo. asyncio.TaskGroup (3.11+) ejecuta tasks concurrentemente — si una falla, cancela todas. Sustituye gather() con mejor manejo de errores.

Lanzar Excepciones
# raise
def dividir(a, b):
    if b == 0:
        raise ValueError("Divisor no pode ser zero")
    return a / b

# Re-lanzar
try:
    procesar()
except Exception as e:
    log(e)
    raise  # re-lanza la misma excepción

# Excepción personalizada
class SaldoInsuficiente(Exception):
    def __init__(self, saldo, valor):
        self.saldo = saldo
        self.valor = valor
        super().__init__(
            f"Saldo {saldo} < {valor}"
        )

raise SaldoInsuficiente(50, 100)

raise lanza excepciones. Crea clases con extends Exception para errores específicos. raise sin argumentos re-lanza la excepción actual. Incluye mensajes claros y datos relevantes.

pathlib
from pathlib import Path

# Crear paths
p = Path("/var/www/datos")
p = Path.home() / "Documentos" / "file.txt"

# Información
p.name        # "file.txt"
p.stem        # "file"
p.suffix      # ".txt"
p.parent      # Path("Documentos")
p.exists()    # True/False
p.is_file()   # True/False
p.is_dir()    # True/False

# Operaciones
p.mkdir(parents=True, exist_ok=True)
p.touch()
p.rename("nuevo.txt")
p.unlink()    # eliminar archivo

# Listar
for f in Path(".").glob("*.py"):
    print(f)
for f in Path(".").rglob("*.txt"):
    print(f)  # recursivo

pathlib.Path es la forma moderna de trabajar con paths. / concatena rutas. .glob() encuentra archivos. .rglob() es recursivo. .read_text()/.write_text() para I/O rápido. Sustituye os.path.

Context Managers (with)
# with: garantiza cleanup
with open("fichero.txt") as f:
    contenido = f.read()
# ¡Archivo cerrado automáticamente!

# Múltiples recursos
with open("a.txt") as a, open("b.txt") as b:
    datos = a.read() + b.read()

# Context manager personalizado
class Timer:
    def __enter__(self):
        import time
        self.inicio = time.perf_counter()
        return self

    def __exit__(self, *args):
        import time
        self.fin = time.perf_counter()
        print(f"Tiempo: {self.fin - self.inicio:.4f}s")

with Timer():
    proceso_lento()

# contextlib
from contextlib import contextmanager

with garantiza cleanup incluso con excepciones. __enter__/__exit__ definen el protocolo. Ideal para archivos, conexiones BD, locks. @contextmanager crea context managers con yield.

Logging
import logging

# Configuración básica
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("app.log"),
        logging.StreamHandler(),
    ]
)

logger = logging.getLogger(__name__)

logger.debug("Detalle técnico")
logger.info("Operación normal")
logger.warning("Atención!")
logger.error("Algo falhou")
logger.critical("Error grave!")

# Con excepción
try:
    1 / 0
except Exception:
    logger.exception("Error na división")

logging sustituye print() para debug. Niveles: DEBUG, INFO, WARNING, ERROR, CRITICAL. getLogger(__name__) por módulo. logger.exception() incluye el traceback automáticamente.

Operaciones con Archivos
# Leer
with open("datos.txt", "r", encoding="utf-8") as f:
    contenido = f.read()       # todo
    lineas = f.readlines()    # lista de líneas

# Leer línea a línea (eficiente en memoria)
with open("grande.log") as f:
    for linea in f:
        procesar(linea)

# Escribir
with open("output.txt", "w") as f:
    f.write("Hola\n")
    f.writelines(["a\n", "b\n"])

# Append
with open("log.txt", "a") as f:
    f.write("nueva linea\n")

# pathlib (moderno)
from pathlib import Path
p = Path("datos.txt")
p.read_text(encoding="utf-8")
p.write_text("contenido")
p.exists()

open() con with cierra automáticamente. Modos: r leer, w escribir, a append. encoding="utf-8" para acentos. pathlib.Path es la forma moderna y OOP.

assert y Debugging
# assert: verifica condición en desarrollo
def dividir(a, b):
    assert b != 0, "Divisor no pode ser zero"
    return a / b

# assert con mensaje
assert isinstance(nombre, str), f"Esperado str, got {type(nombre)}"

# breakpoint() (Python 3.7+)
def proceso(x):
    resultado = x * 2
    breakpoint()  # abre el debugger (pdb)
    return resultado + 1

# En pdb:
# n = next, s = step, c = continue
# p variable = print
# l = listar código

# python -m pdb script.py
# PYTHONBREAKPOINT=0 lo desactiva

assert verifica condiciones en desarrollo (se elimina con -O). breakpoint() abre el debugger pdb. Comandos: n next, s step, c continue, p print. No uses assert para validación de input.

Módulos e Pacotes


6 cards
Importar Módulos
# Formas de importar
import os
import os.path
from os import path
from os.path import join, exists
import numpy as np
from typing import List, Optional

# Import condicional
try:
    import ujson as json
except ImportError:
    import json

# Import relativo (dentro de paquete)
from . import utils
from ..models import User

# __all__ controla from x import *
# __init__.py:
__all__ = ["ClasseA", "funcion_b"]

import módulo importa todo. from x import y importa algo específico. as crea alias. Import relativo con . y ... __all__ controla exports. Evita from x import *.

Crear un Paquete
# Estructura:
# mi_paquete/
#   __init__.py
#   core.py
#   utils/
#     __init__.py
#     helpers.py

# __init__.py (exporta API pública)
from .core import ClassePrincipal
from .utils.helpers import funcion_util

__all__ = ["ClassePrincipal", "funcion_util"]
__version__ = "1.0.0"

# Uso:
from mi_paquete import ClassePrincipal
from mi_paquete.utils import helpers

# __name__ == "__main__"
if __name__ == "__main__":
    # Solo se ejecuta como script
    print("Executado diretamente")

Los paquetes necesitan __init__.py. __all__ define exports públicos. __name__ == "__main__" verifica si es el script principal. Estructura: carpeta con __init__.py + módulos. __version__ por convención.

pip y venv
# Ambiente virtual
python -m venv .venv
source .venv/bin/activate   # Linux/Mac
.venv\Scripts\activate      # Windows
deactivate

# pip
pip install requests
pip install "flask>=2.0,<3.0"
pip install -r requirements.txt
pip freeze > requirements.txt
pip uninstall requests
pip list
pip show requests

# pyproject.toml (moderno)
# [project]
# dependencies = [
#     "requests>=2.28",
#     "flask>=2.0",
# ]

# pip-tools / poetry / uv
pip install pip-tools
pip-compile pyproject.toml

venv aísla dependencias por proyecto. pip install instala paquetes. requirements.txt lista dependencias. pyproject.toml es el formato moderno. Herramientas: poetry, uv, pip-tools.

__name__ y Scripts
# script.py
def main():
    print("Programa principal")

if __name__ == "__main__":
    main()

# Cuando se ejecuta: __name__ == "__main__"
# Cuando se importa: __name__ == "script"

# CLI con argparse
import argparse

parser = argparse.ArgumentParser(description="Tool")
parser.add_argument("nombre", help="Nombre")
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("-n", "--count", type=int, default=1)
args = parser.parse_args()

if args.verbose:
    print(f"Hola {args.nombre}!" * args.count)

if __name__ == "__main__" separa código de ejecución vs importación. argparse crea CLI con flags y ayuda automática. action="store_true" para flags booleanas. type=int convierte argumentos.

Módulos Estándar Útiles
import os, sys, json, math, random
import datetime, collections, itertools
import re, pathlib, subprocess, hashlib

# os / pathlib
os.getcwd()
os.environ.get("HOME")
Path.home()

# sys
sys.argv        # argumentos CLI
sys.version     # versión Python
sys.exit(1)     # salir con código

# math
math.pi         # 3.14159...
math.sqrt(16)   # 4.0
math.ceil(3.2)  # 4

# random
random.randint(1, 100)
random.choice([1, 2, 3])
random.shuffle(lista)

# hashlib
hashlib.sha256(b"texto").hexdigest()

La stdlib de Python es enorme. os/pathlib para archivos. sys para sistema. math para matemáticas. random para aleatoriedad. json, re, datetime, collections, itertools.

datetime
from datetime import datetime, date, timedelta

# Ahora
ahora = datetime.now()
hoy = date.today()

# Formatear
ahora.strftime("%d/%m/%Y %H:%M")
# "25/12/2024 14:30"

# Parse
dt = datetime.strptime("25/12/2024", "%d/%m/%Y")

# Aritmética
mañana = hoy + timedelta(days=1)
in_1h = ahora + timedelta(hours=1)
diferencia = dt2 - dt1
diferencia.days  # días

# ISO
datetime.now().isoformat()
datetime.fromisoformat("2024-12-25T14:30:00")

# Comparación
if ahora > dt:
    print("Pasado")

datetime.now() da la fecha/hora actual. strftime() formatea, strptime() hace parse. timedelta para aritmética. isoformat()/fromisoformat() para formato ISO. La resta da un timedelta.

Dicas e Boas Práticas


8 cards
Operador Walrus (:=)
# Asignación en expresión (Python 3.8+)
if (n := len(datos)) > 10:
    print(f"Lista grande: {n}")

# En while loops
while (linea := input("> ")) != "salir":
    procesar(linea)

# En comprehensions
resultados = [
    y for x in datos
    if (y := procesar(x)) is not None
]

# Con match (3.10+)
match datos:
    case {"nombre": nombre} if (n := len(nombre)) > 3:
        print(f"Nombre largo: {nombre} ({n})")

# Sin walrus (antes):
# n = len(datos)
# if n > 10: ...

:= (walrus) asigna y retorna valor en una expresión. Evita líneas extra de asignación. Útil en while, if y comprehensions. No abuses — úsalo solo cuando mejora legibilidad.

Anti-patrones a Evitar
# 1. Mutable como default (¡BUG!)
def mau(lista=[]):      # ¡NUNCA!
    lista.append(1)
    return lista

def bom(lista=None):
    if lista is None:
        lista = []
    lista.append(1)
    return lista

# 2. Comparar con None usando ==
if x == None:    # malo
if x is None:    # bueno

# 3. Loop con índice
for i in range(len(lista)):  # malo
    print(lista[i])
for item in lista:           # bueno
    print(item)

# 4. Import *
from módulo import *  # malo (contamina namespace)

# 5. except genérico
except:  # malo (captura todo, hasta KeyboardInterrupt)
except Exception:  # mejor

Anti-patrones: default mutable en funciones (usa None), == None (usa is None), loop con índice (usa for item), import *, except: genérico. Sigue PEP 8 y usa ruff.

PEP 8 y Estilo
# Nombres
mi_variable = 1      # snake_case
MI_CONSTANTE = 100   # UPPER_SNAKE
class MiClase:      # PascalCase
    pass
def mi_funcion():     # snake_case
    pass

# Formateo
x = 1 + 2              # espacios en operadores
lista = [1, 2, 3]      # espacio tras coma
func(a, b, c=3)        # sin espacio en = de kwargs

# Imports (inicio del archivo)
import os               # stdlib
import requests         # terceros
from . import utils     # local

# Líneas máx: 79 (código) / 72 (docs)
# Herramientas: black, ruff, flake8, isort

PEP 8 es la guía de estilo. Nombres: snake_case para variables/funciones, PascalCase para clases, UPPER_SNAKE para constantes. Herramientas: black formatea, ruff linta, isort ordena imports.

CLI con argparse y click
# argparse (stdlib)
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("fichero")
parser.add_argument("-v", "--verbose", action="store_true")
parser.add_argument("-o", "--output", default="out.txt")
parser.add_argument("-n", type=int, default=10)
args = parser.parse_args()

# click (tercero, más elegante)
import click

@click.command()
@click.argument("fichero")
@click.option("-v", "--verbose", is_flag=True)
@click.option("-n", "--count", default=10, type=int)
def procesar(fichero, verbose, count):
    """Processa um fichero."""
    click.echo(f"Processando {fichero} x{count}")

if __name__ == "__main__":
    procesar()

argparse (stdlib) para CLIs simples. click (tercero) es más elegante con decorators. action="store_true" para flags. type=int convierte. Docstring se convierte en --help. Para CLIs complejos: typer (type hints).

Trucos Idiomáticos
# Comparaciones encadenadas
if 0 < x < 100:
    pass

# Ternario
status = "par" if n % 2 == 0 else "impar"

# Unpacking
primero, *resto = [1, 2, 3, 4, 5]

# dict.get con default
valor = d.get("clave", "default")

# any / all
if all(x > 0 for x in nums):
    print("Todos positivos")

# f-string debug (3.8+)
x = 42
print(f"{x = }")  # "x = 42"

# Invertir string
"invertir"[::-1]  # "retrevni"

# Merge dicts (3.9+)
merged = d1 | d2

# enumerate con start
for i, item in enumerate(lista, start=1):
    print(f"{i}. {item}")

Trucos idiomáticos: comparaciones encadenadas, ternario, unpacking, dict.get(), any()/all(). f"{x = }" muestra nombre y valor (debug). [::-1] invierte. | hace merge de dicts.

Dataclasses Avanzadas
from dataclasses import dataclass, field, asdict, astuple

@dataclass(order=True)
class Producto:
    nombre: str = field(compare=False)
    precio: float
    stock: int = 0
    tags: list[str] = field(default_factory=list)

    @property
    def disponible(self) -> bool:
        return self.stock > 0

p = Producto("Libro", 19.99, 5)
asdict(p)    # {"nombre": "Libro", "precio": 19.99, ...}
astuple(p)   # ("Libro", 19.99, 5, [])

# Ordenación por precio (order=True)
productos = [Producto("A", 30), Producto("B", 10)]
sorted(productos)  # ordena por precio

# slots=True (3.10+)
@dataclass(slots=True)
class Ponto:
    x: float
    y: float

@dataclass(order=True) genera métodos de comparación. field(compare=False) excluye campo de la ordenación. asdict()/astuple() convierten. slots=True (3.10+) reduce memoria. default_factory para mutables.

Rendimiento y Optimización
# timeit: medir tiempo
import timeit
timeit.timeit('"-".join(map(str, range(100)))', number=10000)

# cProfile: profiling
import cProfile
cProfile.run("mi_funcion()")

# Consejos de rendimiento:
# 1. list comprehension > loop con append
# 2. dict/set lookup O(1) > list O(n)
# 3. join() > concatenación en loop
# 4. generators > listas para datos grandes
# 5. lru_cache para funciones puras

# __slots__ para muchas instancias
# local variables > global variables

# collections.deque > list para filas
# numpy para arrays numéricos

timeit mide tiempo de ejecución. cProfile muestra profiling detallado. Consejos: usa comprehensions, dict/set para lookups, join() para strings, generators para datos grandes, @lru_cache para memoization.

Type Hints Avanzados
from typing import TypeVar, Generic, Protocol, runtime_checkable

# TypeVar y Generic
T = TypeVar("T")

class Stack(Generic[T]):
    def __init__(self):
        self._items: list[T] = []
    def push(self, item: T) -> None:
        self._items.append(item)
    def pop(self) -> T:
        return self._items.pop()

s: Stack[int] = Stack()
s.push(42)

# Protocol (structural subtyping)
@runtime_checkable
class Renderizavel(Protocol):
    def render(self) -> str: ...

def mostrar(obj: Renderizavel) -> None:
    print(obj.render())

# TypeAlias (3.12+)
# type Vector = list[float]

TypeVar + Generic crean clases genéricas. Protocol define interfaces por estructura (duck typing estático). @runtime_checkable permite isinstance(). Python 3.12: type Alias = ... para type aliases. Verifica con mypy.

Strings e Regex


6 cards
Métodos Avanzados de String
s = "hola mundo python"

s.capitalize()    # "Hola mundo python"
s.title()         # "Hola Mundo Python"
s.swapcase()      # "HOLA MUNDO PYTHON"
s.center(30, "-") # "---hola mundo python---"
s.zfill(20)       # "0000hola mundo python"

# Verificación
s.isalpha()    # False (tiene espacios)
s.isdigit()    # False
s.isalnum()    # False
s.isspace()    # False

# Join y split
"-".join(["a", "b", "c"])  # "a-b-c"
"a,b,c".split(",")         # ["a", "b", "c"]
"a  b  c".split()          # ["a", "b", "c"]

# partition
"nombre@email.com".partition("@")
# ('nombre', '@', 'email.com')

join() es más eficiente que concatenación en loop. split() sin args divide por whitespace. partition() divide en 3 partes. zfill() rellena con ceros. is*() verifica tipo de caracteres.

Template y Textwrap
from string import Template
import textwrap

# Template (seguro para user input)
t = Template("Hola $nombre, tienes $edad anios")
t.substitute(nombre="Ana", edad=30)
# safe_substitute no lanza error si falta
t.safe_substitute(nombre="Ana")

# textwrap
texto = "Texto muy largo que necesita ser roto em varias lineas para quedar legible."
textwrap.fill(texto, width=40)
textwrap.dedent("""
    Remove indentación
    comum de todas as lineas
""")
textwrap.shorten(texto, width=30)  # "Texto muy largo que..."

# repr vs str
repr("hola\nmundo")  # "'hola\\nmundo'" (debug)
str("hola\nmundo")   # output formateado

Template es seguro para input del usuario (sin inyección). textwrap.fill() quiebra texto en líneas. textwrap.dedent() elimina indentación común. repr() muestra representación debug, str() muestra formateado.

Regex Básico
import re

texto = "Contacto: ana@mail.com ou 912-345-678"

# search: primera ocurrencia
m = re.search(r"\d{3}-\d{3}-\d{3}", texto)
if m:
    print(m.group())  # "912-345-678"

# findall: todas
emails = re.findall(r"[\w.]+@[\w.]+", texto)
# ["ana@mail.com"]

# sub: sustituir
re.sub(r"\d", "X", "abc123")  # "abcXXX"

# match: inicio de la string
re.match(r"\d+", "123abc")  # Match
re.match(r"\d+", "abc123")  # None

# compile (reutilizar)
padrao = re.compile(r"\b\w+@\w+\.\w+\b")
padrao.findall(texto)

re.search() encuentra la primera ocurrencia. re.findall() retorna todas. re.sub() sustituye. re.match() solo al inicio. re.compile() para reutilizar patrones. Usa raw strings (r"...").

difflib y Comparación
import difflib

# Diferencia entre textos
texto1 = "Hola Mundo\nPython 3".splitlines()
texto2 = "Hola Mundo\nPython 3.12".splitlines()

diff = difflib.unified_diff(texto1, texto2, lineterm="")
print("\n".join(diff))

# Similitud
difflib.SequenceMatcher(None, "python", "pyton").ratio()
# 0.909...

# Sugerencias (close matches)
difflib.get_close_matches("pytho", ["python", "java", "perl"])
# ["python"]

# ndiff: diferencia carácter a carácter
list(difflib.ndiff("abc", "axc"))
# ['  a', '- b', '+ x', '  c']

difflib compara secuencias. unified_diff() muestra diferencias en formato unified. SequenceMatcher.ratio() da similitud (0-1). get_close_matches() sugiere correcciones. ndiff() muestra diferencia carácter a carácter.

Regex Avanzado
import re

# Grupos nombrados
m = re.search(
    r"(?P<nombre>\w+)@(?P<dominio>[\w.]+)",
    "ana@mail.com"
)
m.group("nombre")     # "ana"
m.group("dominio")  # "mail.com"

# Lookahead / Lookbehind
re.findall(r"\w+(?=@)", "ana@mail rui@test")
# ["ana", "rui"] (antes de @)

# Cuantificadores
# \d{2,4}   2 a 4 dígitos
# \w+       1 o más
# \s*       0 o más espacios
# .+?       non-greedy

# Flags
re.findall(r"hola", texto, re.IGNORECASE)
re.split(r"\s+", texto, flags=re.MULTILINE)

# sub con función
re.sub(r"\d+", lambda m: str(int(m.group())*2), "a1b2")
# "a2b4"

Grupos nombrados: (?P...). Lookahead: (?=...). re.IGNORECASE ignora mayúsculas. sub() acepta función como sustitución. non-greedy con ? tras el cuantificador.

Encoding y Unicode
# Las Strings son Unicode por defecto
texto = "Hola 🌍"
len(texto)  # 7 (caracteres)

# Encode/Decode
bytes_data = texto.encode("utf-8")
texto2 = bytes_data.decode("utf-8")

# Leer archivo con encoding
with open("fichero.txt", encoding="utf-8") as f:
    contenido = f.read()

# Detectar encoding (chardet)
# import chardet
# resultado = chardet.detect(bytes_data)

# Unicode escapes
"\u0041"      # "A"
"\U0001F600"  # "😀"
chr(65)       # "A"
ord("A")      # 65

# Normalización
import unicodedata
unicodedata.normalize("NFC", texto)

Python 3: las strings son Unicode. encode() convierte a bytes. decode() convierte bytes a string. Siempre especifica encoding="utf-8" en archivos. chr()/ord() convierten carácter/código.

Async e Concorrência


6 cards
async / await
import asyncio

async def fetch_datos(url: str) -> str:
    print(f"Iniciar: {url}")
    await asyncio.sleep(1)  # simula I/O
    print(f"Done: {url}")
    return f"Datos de {url}"

async def main():
    # Secuencial (3s)
    r1 = await fetch_datos("api/1")
    r2 = await fetch_datos("api/2")

    # Concurrente (1s!)
    resultados = await asyncio.gather(
        fetch_datos("api/1"),
        fetch_datos("api/2"),
        fetch_datos("api/3"),
    )
    print(resultados)

asyncio.run(main())

async def define una corutina. await pausa hasta completar. asyncio.gather() ejecuta concurrentemente. asyncio.run() inicia el event loop. Ideal para I/O (HTTP, BD, archivos). No uses time.sleep() — usa asyncio.sleep().

multiprocessing
from multiprocessing import Pool, cpu_count
import multiprocessing as mp

# Pool de procesos
def cuadrado(n):
    return n ** 2

if __name__ == "__main__":
    with Pool(processes=cpu_count()) as pool:
        resultados = pool.map(cuadrado, range(10))
        # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

    # apply_async (no bloqueante)
    with Pool() as pool:
        r = pool.apply_async(cuadrado, (10,))
        print(r.get(timeout=5))  # 100

    # starmap (múltiples args)
    def potencia(base, exp):
        return base ** exp
    with Pool() as pool:
        pool.starmap(potencia, [(2,3), (3,2)])

print(f"CPUs: {cpu_count()}")

multiprocessing evita el GIL con procesos separados. Pool.map() distribuye trabajo. cpu_count() da el numero de colores. if __name__ == "__main__" obligatorio en Windows. Para I/O, prefiere threading o asyncio.

asyncio — Patrones
import asyncio

# Task: crear y cancelar
async def tarea():
    await asyncio.sleep(10)

async def main():
    task = asyncio.create_task(tarea())
    task.cancel()

# Semaphore: limitar concurrencia
sem = asyncio.Semaphore(5)

async def fetch_limitado(url):
    async with sem:
        await asyncio.sleep(1)
        return url

# Queue: productor/consumidor
async def produtor(queue):
    for i in range(10):
        await queue.put(i)
    await queue.put(None)

async def consumidor(queue):
    while (item := await queue.get()) is not None:
        procesar(item)

create_task() agenda una corutina. Semaphore limita concurrencia. asyncio.Queue para productor/consumidor. task.cancel() cancela. Usa async with para recursos asíncronos.

async for y async with
import asyncio

# async for: iterador asíncrono
async def leer_lineas(fichero):
    async with aiofiles.open(fichero) as f:
        async for linea in f:
            yield linea.strip()

async def main():
    async for linea in leer_lineas("datos.txt"):
        procesar(linea)

# async with: context manager asíncrono
import aiohttp

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            return await resp.json()

# async generator
async def ticker(intervalo, n):
    for i in range(n):
        await asyncio.sleep(intervalo)
        yield i

async for itera sobre iteradores asíncronos. async with para context managers asíncronos (conexiones, archivos). async generators con yield en funciones async. Usa librerías como aiohttp y aiofiles para I/O asíncrono.

Threading
from threading import Thread, Lock
import threading

# Thread básica
def worker(nombre):
    print(f"Thread {nombre} a correr")

t = Thread(target=worker, args=("A",))
t.start()
t.join()  # espera a que termine

# Múltiples threads
threads = []
for i in range(5):
    t = Thread(target=worker, args=(str(i),))
    threads.append(t)
    t.start()
for t in threads:
    t.join()

# Lock (thread-safe)
lock = Lock()
contador = 0

def incrementar():
    global contador
    with lock:
        contador += 1

# GIL: los threads de Python no paralelizan CPU
# Úsalo para I/O, no para CPU-bound

Thread para operaciones I/O concurrentes. Lock protege datos compartidos. t.join() espera a que termine. El GIL impide paralelismo real en CPU — para CPU-bound usa multiprocessing.

concurrent.futures
from concurrent.futures import (
    ThreadPoolExecutor,
    ProcessPoolExecutor,
    as_completed
)

# ThreadPool (I/O-bound)
def fetch(url):
    import requests
    return requests.get(url).status_code

with ThreadPoolExecutor(max_workers=5) as pool:
    urls = ["http://a.com", "http://b.com"]
    results = list(pool.map(fetch, urls))

# ProcessPool (CPU-bound)
def cpu_work(n):
    return sum(i*i for i in range(n))

with ProcessPoolExecutor() as pool:
    futures = [pool.submit(cpu_work, n) for n in nums]
    for f in as_completed(futures):
        print(f.result())

# as_completed: resultados por orden de finalización

ThreadPoolExecutor para I/O (HTTP, archivos). ProcessPoolExecutor para CPU (cálculos). pool.map() aplica función a lista. as_completed() retorna por orden de finalización. API de alto nivel sobre threads/procesos.

Web e APIs


9 cards
requests
import requests

# GET
r = requests.get("https://api.ejemplo.com/users")
r.status_code   # 200
r.json()        # parse JSON
r.headers       # headers
r.text          # cuerpo raw

# Con parámetros
r = requests.get(url, params={"page": 1, "limit": 10})

# POST
r = requests.post(url, json={"nombre": "Ana"})
r = requests.post(url, data={"nombre": "Ana"})

# Headers y auth
r = requests.get(url, headers={"Authorization": f"Bearer {token}"})

# Timeout y sesión
r = requests.get(url, timeout=5)
with requests.Session() as s:
    s.get(url)  # reutiliza conexión

requests es la librería HTTP estándar. .json() hace parse automático. params para query string. json= envía JSON con header correcto. timeout evita hangs. Session() reutiliza conexiones.

Web Scraping
import requests
from bs4 import BeautifulSoup

# Fetch + parse
r = requests.get("https://ejemplo.com")
soup = BeautifulSoup(r.text, "html.parser")

# Selectores CSS
título = soup.select_one("h1").text
links = soup.select("a.link")
for a in links:
    print(a["href"], a.text)

# Por tag y atributo
soup.find("div", class_="contenido")
soup.find_all("p", limit=5)

# lxml (más rápido)
soup = BeautifulSoup(r.text, "lxml")

# Para sitios dinámicos: selenium / playwright
# from playwright.sync_api import sync_playwright

BeautifulSoup hace parse de HTML. select() usa selectores CSS. find()/find_all() por tag/atributo. lxml es un parser más rápido. Para JavaScript dinámico: playwright o selenium.

httpx y Async HTTP
import httpx

# Síncrono (como requests)
r = httpx.get("https://api.ejemplo.com/users")
r.json()

# Async
async def fetch_all(urls):
    async with httpx.AsyncClient() as client:
        tasks = [client.get(url) for url in urls]
        responses = await asyncio.gather(*tasks)
        return [r.json() for r in responses]

# Con timeout y headers
async with httpx.AsyncClient(
    timeout=10.0,
    headers={"Authorization": f"Bearer {token}"},
) as client:
    r = await client.post(
        url,
        json={"nombre": "Ana"},
    )

# HTTP/2
async with httpx.AsyncClient(http2=True) as c:
    r = await c.get(url)

httpx es una alternativa moderna a requests con soporte async. AsyncClient para requests concurrentes. Soporta HTTP/2. API compatible con requests. Ideal para FastAPI y aplicaciones asíncronas. asyncio.gather() para múltiples requests.

Flask — API Básica
from flask import Flask, jsonify, request

app = Flask(__name__)

@app.route("/api/users", methods=["GET"])
def listar_users():
    users = [{"id": 1, "nombre": "Ana"}]
    return jsonify(users)

@app.route("/api/users", methods=["POST"])
def crear_user():
    datos = request.get_json()
    nombre = datos.get("nombre")
    return jsonify({"id": 2, "nombre": nombre}), 201

@app.route("/api/users/<int:user_id>")
def obtener_user(user_id):
    return jsonify({"id": user_id})

if __name__ == "__main__":
    app.run(debug=True, port=5000)

Flask es un micro-framework web. @app.route() define endpoints. jsonify() retorna JSON. request.get_json() lee el body. methods=["POST"] define verbos HTTP. debug=True para desarrollo.

Variables de Entorno
import os
from pathlib import Path

# Leer variable
db_host = os.environ.get("DB_HOST", "localhost")
api_key = os.environ["API_KEY"]  # KeyError si no existe

# python-dotenv
# pip install python-dotenv
from dotenv import load_dotenv
load_dotenv()  # carga .env

# .env:
# DB_HOST=localhost
# DB_PORT=5432
# SECRET_KEY=abc123

# Config pattern
class Config:
    DB_HOST = os.getenv("DB_HOST", "localhost")
    DB_PORT = int(os.getenv("DB_PORT", "5432"))
    DEBUG = os.getenv("DEBUG", "false").lower() == "true"

config = Config()

os.environ.get() lee variables de entorno. python-dotenv carga el archivo .env. Nunca hagas commit de .env (agrégalo al .gitignore). Patrón: clase Config con defaults. os.getenv() = os.environ.get().

FastAPI
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class UserCreate(BaseModel):
    nombre: str
    email: str
    edad: int | None = None

@app.get("/users/{user_id}")
async def obtener_user(user_id: int):
    if user_id > 100:
        raise HTTPException(404, "No encontrado")
    return {"id": user_id}

@app.post("/users", status_code=201)
async def crear_user(user: UserCreate):
    return {"id": 1, **user.model_dump()}

# Ejecutar: uvicorn main:app --reload
# Docs automáticos: /docs (Swagger)

FastAPI es moderno y asíncrono. Pydantic valida datos automáticamente. Type hints generan validación y docs. HTTPException para errores. Docs automáticos en /docs. Ejecuta con uvicorn.

HTTP Methods y Status
# Verbos HTTP
# GET    - leer recurso
# POST   - crear recurso
# PUT    - sustituir recurso
# PATCH  - actualización parcial
# DELETE - eliminar recurso

# Códigos de status
# 200 OK
# 201 Created
# 204 No Content
# 400 Bad Request
# 401 Unauthorized
# 403 Forbidden
# 404 Not Found
# 422 Unprocessable Entity
# 500 Internal Server Error

# En Flask
from flask import jsonify
return jsonify({"error": "No encontrado"}), 404

# En FastAPI
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="No encontrado")

Verbos HTTP: GET leer, POST crear, PUT sustituir, PATCH parcial, DELETE eliminar. Códigos: 2xx éxito, 4xx error del cliente, 5xx error del servidor. 401 = no autenticado, 403 = sin permiso.

JSON API — Buenas Prácticas
# Respuestas estandarizadas
def respuesta_ok(datos, msg="Éxito"):
    return {"status": "ok", "message": msg, "data": datos}

def respuesta_error(msg, código=400):
    return {"status": "error", "message": msg}, código

# Validación con Pydantic
from pydantic import BaseModel, EmailStr, field_validator

class UserIn(BaseModel):
    nombre: str
    email: EmailStr
    edad: int

    @field_validator("edad")
    @classmethod
    def edad_valida(cls, v):
        if v < 0 or v > 150:
            raise ValueError("Edad inválida")
        return v

# Paginación
def paginar(items, page=1, per_page=20):
    start = (page - 1) * per_page
    return items[start:start + per_page]

APIs: respuestas estandarizadas con status, message, data. Pydantic valida input. @field_validator para reglas custom. Paginación con page/per_page. Códigos HTTP correctos: 200, 201, 400, 404, 500.

Testing con pytest
import pytest

# Test básico
def test_suma():
    assert suma(2, 3) == 5

# Con fixtures
@pytest.fixture
def client():
    app.config["TESTING"] = True
    with app.test_client() as c:
        yield c

def test_listar(client):
    resp = client.get("/api/users")
    assert resp.status_code == 200
    assert len(resp.json) > 0

# Parametrize
@pytest.mark.parametrize("n,esperado", [
    (1, 1), (2, 4), (3, 9),
])
def test_cuadrado(n, esperado):
    assert cuadrado(n) == esperado

# Excepciones
with pytest.raises(ValueError):
    dividir(1, 0)

pytest es el framework de tests estándar. assert simple (sin self.assertEqual). @pytest.fixture para setup compartido. @pytest.mark.parametrize testa múltiples casos. pytest.raises() verifica excepciones. Ejecuta con pytest -v.

Instalação e Setup


9 cards
Instalar Python
# Windows:
# 1. Descargar en python.org/downloads
# 2. Marcar "Add Python to PATH" (¡importante!)
# 3. Instalar

# macOS (Homebrew):
brew install python

# Linux (Ubuntu/Debian):
sudo apt update
sudo apt install python3 python3-pip

# Verificar instalación:
python --version     # Windows
python3 --version    # macOS/Linux
pip --version

Descarga el instalador oficial en python.org. En Windows marca Add Python to PATH para usar python en la terminal. En macOS usa Homebrew. En Linux usa apt. python3 es el comando estándar en sistemas Unix.

Entornos Virtuales (venv)
# Crear entorno virtual:
python -m venv .venv

# Activar:
# Windows:
.venv\Scripts\activate
# macOS/Linux:
source .venv/bin/activate

# El prompt cambia: (.venv) $
# Instalar paquetes aislados:
pip install requests

# Desactivar:
deactivate

# ¡Cada proyecto debe tener su
# propio entorno virtual!

venv crea entornos aislados — cada proyecto tiene sus propias dependencias sin conflictos. python -m venv .venv lo crea. activate lo activa (el prompt muestra (.venv)). deactivate lo desactiva. Esencial para proyectos reales.

pyproject.toml (moderno)
# pyproject.toml (estándar moderno):
[project]
name = "mi-proyecto"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = [
    "requests>=2.31",
    "flask>=2.3",
]

[project.optional-dependencies]
dev = ["pytest", "black", "ruff"]

# Poetry (alternativa a pip+venv):
# pip install poetry
poetry new mi-proyecto
poetry add requests
poetry install
poetry run python main.py

# Instala dependencias de dev:
poetry install --with dev

pyproject.toml es el estándar moderno de configuración (sustituye a setup.py). Poetry gestiona dependencias y entornos virtuales de forma integrada. poetry add instala y registra. Alternativas: uv (muy rápido) y pdm. Los proyectos nuevos deben usar pyproject.toml.

Primer Script
# hola.py
print("Hola, Mundo!")

# Ejecutar en la terminal:
# python hola.py        (Windows)
# python3 hola.py       (macOS/Linux)

# Script con entrada del usuario:
nombre = input("Qual es o tu nombre? ")
print(f"Hola, {nombre}!")

# Los comentarios empiezan con #
# Python usa indentación (4 espacios)
# en vez de llaves {}

Crea un archivo .py y ejecútalo con python fichero.py. print() muestra output. input() lee del teclado. Las f-strings (f"...{var}") interpolan variables. La indentación define los bloques de código.

requirements.txt
# requirements.txt (ejemplo):
requests==2.31.0
flask>=2.3.0
numpy~=1.24
django

# Operadores de versión:
# ==  versión exacta
# >=  mayor o igual
# <=  menor o igual
# ~=  compatible (mismo minor)
# !=  diferente de

# Generar a partir del entorno:
pip freeze > requirements.txt

# Instalar todo:
pip install -r requirements.txt

requirements.txt lista las dependencias del proyecto con versiones. == fija la versión exacta. >= permite más recientes. ~= permite solo patches. Genera con pip freeze e instala con pip install -r. Garantiza reproducibilidad.

REPL Interactivo
# Iniciar el REPL (Read-Eval-Print Loop):
# python        (Windows)
# python3       (macOS/Linux)

>>> 2 + 3
5
>>> "python".upper()
'PYTHON'
>>> import math
>>> math.sqrt(16)
4.0
>>> exit()      # o Ctrl+D para salir

# IPython (REPL mejorado):
# pip install ipython
# ipython
# Soporta autocomplete, histórico,
# comandos mágicos (%timeit, %run)

El REPL permite probar código línea a línea — ideal para experimentar. >>> es el prompt. exit() o Ctrl+D para salir. IPython es una versión más potente con autocomplete y comandos mágicos.

IDEs y Editores
# VS Code (recomendado):
# 1. Instalar la extensión "Python" (Microsoft)
# 2. Seleccionar el intérprete:
#    Ctrl+Shift+P > "Python: Select Interpreter"
# 3. Ejecutar: botón ▶ o Ctrl+F5

# PyCharm (IDE completo):
# Community (gratis) o Professional
# Debugging, tests y frameworks integrados

# Jupyter Notebook (datos/ciencia):
pip install notebook
jupyter notebook
# Abre en el navegador — celdas interactivas

# Ejecutar desde la terminal:
python script.py

VS Code con la extensión Python es la opción más popular. PyCharm es una IDE completa con debugging avanzado. Jupyter Notebook es ideal para ciencia de datos con celdas interactivas. Todos soportan autocomplete y linting.

pip (Gestor de Paquetes)
# Instalar paquete:
pip install requests
pip install numpy pandas matplotlib

# Versión específica:
pip install django==4.2
pip install "flask>=2.0"

# Actualizar:
pip install --upgrade requests

# Listar instalados:
pip list
pip show requests

# Eliminar:
pip uninstall requests

# Guardar/instalar dependencias:
pip freeze > requirements.txt
pip install -r requirements.txt

pip es el gestor de paquetes estándar de Python. pip install instala desde PyPI. pip freeze > requirements.txt exporta las dependencias. pip install -r las reinstala. Usa pip3 en sistemas Unix con Python 2 y 3.

Estructura de Proyecto
mi_proyecto/
├── .venv/              # entorno virtual
├── mi_proyecto/        # paquete principal
│   ├── __init__.py
│   ├── main.py
│   └── utils.py
├── tests/              # tests
│   └── test_main.py
├── requirements.txt    # dependencias
├── .gitignore          # ignorar .venv/
└── README.md

# __init__.py marca la carpeta
# como un paquete Python

# .gitignore esencial:
# .venv/
# __pycache__/
# *.pyc

Estructura estándar: carpeta del paquete con __init__.py, tests/ para tests, requirements.txt para dependencias. .venv/ nunca va al git. __pycache__/ son archivos compilados — ignorarlos. __init__.py marca la carpeta como paquete.