Skip to content
This page has been auto-translated and may contain errors.View in English

Módulos y la biblioteca estándar ​

Python viene con una enorme colección de herramientas listas para usar: aleatoriedad, matemáticas, fechas, rutas de archivos, y mucho más. Estas herramientas viven en módulos, y las traes a tu código con import. Ya usaste import json en el capítulo de Archivos y excepciones. Este capítulo cubre los imports en detalle e introduce las partes más útiles de la biblioteca estándar.

La biblioteca estándar de Python ofrece soluciones probadas y documentadas para problemas comunes. Los módulos son la unidad de organización del código: cada archivo es un módulo, y cada directorio con un __init__.py es un paquete. El sistema de import encuentra los módulos, los compila si es necesario, y los guarda en caché en sys.modules para que solo se carguen una vez.

Un import hace trabajo real: Python localiza el módulo, lo ejecuta una vez, y luego guarda el resultado en caché en sys.modules (el diccionario de todos los módulos que ya se cargaron), de modo que un segundo import del mismo nombre sea una búsqueda rápida y no una nueva ejecución. import requests asocia el objeto del módulo al nombre requests en tu espacio de nombres actual (la tabla de nombres visibles en ese punto del código). from requests import get ejecuta la misma carga, pero solo asocia get. Las partes que realmente valen la pena en proyectos reales son dónde busca Python (sys.path), cómo los directorios se convierten en paquetes importables (__init__.py), y cómo evitar los imports circulares; todo esto se cubre a continuación.

Importar módulos ​

El import más simple trae todo un módulo y te permite usar su contenido con notación de punto. También puedes importar nombres específicos de un módulo para usarlos directamente sin el prefijo. Los alias abrevian nombres largos.

import module asocia el objeto del módulo al nombre module en el ámbito actual. from module import name solo asocia name. Los alias (import module as alias) son comunes con bibliotecas de terceros. Evita from module import *: contamina el espacio de nombres y hace poco claro de dónde vino cada nombre.

from module import name no se salta ningún trabajo: Python de todos modos carga y ejecuta el módulo completo, y luego solo asocia el único nombre que pediste. La trampa que hay que conocer es el import circular, donde dos módulos intentan importarse mutuamente mientras todavía se están cargando, así que uno de ellos ve un módulo a medio construir y falta un nombre. Las soluciones habituales son mover el import problemático dentro de la función que lo necesita (para que se ejecute después de que ambos módulos terminen de cargarse) o extraer el código compartido a un tercer módulo que ambos puedan importar. Cuando necesitas importar un módulo cuyo nombre solo conoces en tiempo de ejecución, importlib.import_module("name") recibe el nombre como una cadena de texto.

python
import math

math.sqrt(16)     # 4.0
math.pi           # 3.141592653589793
math.floor(3.9)   # 3
math.ceil(3.1)    # 4

Importa nombres específicos de un módulo para usarlos directamente:

python
from math import sqrt, pi

sqrt(16)    # 4.0 (no hace falta el prefijo "math.")
pi          # 3.141592653589793

Dale un alias a un módulo o nombre para abreviarlo:

python
import math as m

m.sqrt(16)    # 4.0

from math import sqrt as square_root
square_root(25)    # 5.0

Los alias son comunes con bibliotecas de terceros populares (import numpy as np, import pandas as pd). Para los módulos de la biblioteca estándar, prefiere usar el nombre completo; hace que el código sea más legible.

JunoImportar módulosimport math trae todo el módulo, y luego accedes con un punto: math.sqrt(16). from math import sqrt toma un solo nombre para que puedas prescindir del prefijo. La palabra clave as renombra un import largo, pero para cosas de la biblioteca estándar el nombre completo se lee más claro.
JunoImportar módulosimport module asocia el objeto del módulo, from module import name asocia un solo nombre, import module as alias lo renombra. Evita from module import *: vuelca todos los nombres en tu ámbito y oculta de dónde vino cada uno.
JunoImportar módulosfrom module import name de todos modos carga el módulo completo, solo conserva un nombre. El problema con el que tarde o temprano te vas a topar es el import circular, donde dos módulos se importan mutuamente a mitad de la carga: mueve ese import dentro de la función, o extrae el código compartido a un tercer módulo. importlib.import_module() está ahí cuando el nombre solo se conoce en tiempo de ejecución.

random ​

El módulo random genera números aleatorios y hace selecciones al azar. Úsalo para juegos, simulaciones, muestreo aleatorio, y cualquier otra cosa que necesite impredecibilidad. Establecer una semilla hace que los resultados sean reproducibles: la misma semilla produce la misma secuencia cada vez.

random usa un generador de números pseudoaleatorios Mersenne Twister. La semilla determina la secuencia completa; la misma semilla siempre produce el mismo resultado. .choice() elige un elemento, .choices() elige con reemplazo, .sample() elige sin reemplazo. .shuffle() modifica la lista en el lugar y devuelve None.

random produce números pseudoaleatorios: parecen aleatorios pero vienen de una fórmula fija impulsada por un valor inicial (la semilla), así que la misma semilla da la misma secuencia en cualquier máquina, que es justamente lo que hace que una prueba sea reproducible. La regla que importa en producción: nunca uses random para nada que una persona pueda explotar, como contraseñas, tokens de sesión o códigos de restablecimiento de contraseña. Es predecible por diseño, así que un atacante que vea suficiente salida puede adivinar el resto. Para eso, recurre al módulo secrets, que toma valores de la aleatoriedad segura del sistema operativo. random.SystemRandom() te da esa misma fuente segura detrás de los nombres de método habituales de random, si preferís no cambiar de API.

python
import random

random.random()              # float entre 0 y 1 (exclusivo)
random.randint(1, 10)        # entero entre 1 y 10 (ambos inclusive)
random.uniform(1.0, 10.0)    # float entre 1.0 y 10.0

colores = ["rojo", "verde", "azul"]
random.choice(colores)       # elige un elemento
random.choices(colores, k=3) # elige k elementos (con reemplazo)
random.sample(colores, k=2)  # elige k elementos (sin reemplazo)

numeros = [1, 2, 3, 4, 5]
random.shuffle(numeros)      # mezcla en el lugar, devuelve None

Para resultados reproducibles (útil en pruebas y ciencia de datos), establece una semilla antes de generar:

python
random.seed(42)
random.randint(1, 100)   # siempre el mismo valor para la semilla 42

La misma semilla produce la misma secuencia cada vez, en cualquier máquina.

Junorandomrandom.choice() elige un elemento de una lista, random.randint(1, 10) da un número entero en un rango, con el extremo incluido. ¿Quieres los mismos resultados en cada ejecución, digamos para una prueba? Llama a random.seed() primero y la secuencia queda fija.
Junorandom.choice() elige uno, .choices() elige con reemplazo, .sample() sin reemplazo. .shuffle() reordena la lista en el lugar y devuelve None, así que nunca escribas nums = random.shuffle(nums). La semilla fija toda la secuencia, y así es como haces que una prueba con aleatoriedad sea repetible.
Junorandomrandom es pseudoaleatorio y predecible por diseño, así que está bien para juegos y pruebas pero está mal para cualquier cosa que un atacante pueda explotar. Usa secrets para tokens, contraseñas y códigos de restablecimiento. Deja random.seed() para los casos reproducibles donde de verdad quieres que la secuencia se repita.

math ​

El módulo math agrega operaciones matemáticas más avanzadas más allá de los operadores aritméticos básicos que viste en el capítulo de Números. Raíces cuadradas, potencias, logaritmos, trigonometría, y valores especiales como pi e infinito, todo está aquí.

math ofrece implementaciones a nivel de C de funciones matemáticas estándar. Ten en cuenta que math.pow() siempre devuelve un float, mientras que el operador ** de Python devuelve int para bases y exponentes enteros. math.log(x, base) calcula el logaritmo en cualquier base; math.log(x) calcula el logaritmo natural.

El detalle que muerde en código real es nan (abreviatura de "not a number", el float que obtienes de resultados indefinidos como 0/0 o un parseo fallido) e inf (infinito). Un nan nunca es igual a nada, ni siquiera a sí mismo, así que x == float("nan") siempre es False. Eso significa que no puedes probar su presencia con ==; usa math.isnan(x), y math.isinf(x) para el infinito. El otro hábito que vale la pena mantener: prefiere math.isclose(a, b) sobre a == b al comparar dos floats, ya que el redondeo de punto flotante hace que la igualdad exacta no sea confiable. Para hacer matemáticas sobre arreglos enteros de números en lugar de uno a la vez, numpy es la herramienta estándar, aunque queda fuera del alcance de esta guía.

python
import math

math.sqrt(25)        # 5.0
math.pow(2, 10)      # 1024.0 (igual que 2 ** 10 pero siempre devuelve float)
math.log(100, 10)    # 2.0 (log base 10)
math.log(math.e)     # 1.0 (log natural)

math.sin(math.pi / 2)   # 1.0
math.cos(0)             # 1.0

math.ceil(3.2)    # 4
math.floor(3.9)   # 3
math.trunc(3.9)   # 3 (igual que int() para positivos)

math.inf          # infinito
math.isnan(float("nan"))   # True
math.isinf(math.inf)       # True
Junomathmath cubre todo lo que va más allá de + - * /: raíces cuadradas, potencias, logaritmos, trigonometría, además de constantes como math.pi. Recurre a él en el momento en que un cálculo necesite más que los operadores básicos, y te ahorras escribir tú mismo la fórmula.
Junomathmath tiene raíces cuadradas, logaritmos y trigonometría. Una cosa a tener en cuenta: math.pow(2, 10) siempre devuelve un float, mientras que 2 ** 10 se mantiene como int para entradas enteras. math.log(x, base) acepta cualquier base, math.log(x) es el logaritmo natural.
Junomath Las trampas son nan e inf: nan nunca es igual a sí mismo, así que pruébalo con math.isnan(), no con ==. Y compara floats con math.isclose() en lugar de ==, porque el redondeo hace que la igualdad exacta no sea confiable. Para arreglos enteros de números, ese es territorio de numpy.

datetime ​

El módulo datetime maneja fechas y horas. datetime.now() te da la fecha y hora actuales. strftime() lo formatea como una cadena de texto. strptime() parsea una cadena de texto para convertirla en un datetime. timedelta representa una duración que puedes sumar o restar.

datetime, date, y timedelta son las clases principales. strftime() formatea un datetime como cadena de texto usando códigos de formato. strptime() parsea una cadena de texto dado un patrón de formato. timedelta admite aritmética: puedes sumar o restar duraciones a fechas y comparar datetimes con <, >, -.

El problema típico en producción es que un datetime.now() sencillo es naive (ingenuo): no tiene una zona horaria asociada, así que en realidad no sabe qué parte del mundo representa. Dos datetimes naive de máquinas distintas parecen comparables pero en silencio no lo son, y esa ambigüedad causa errores de horas desfasadas. La solución es trabajar con datetimes aware (conscientes, que llevan una zona horaria), y estandarizar en UTC todo lo que se almacene o se envíe entre sistemas: datetime.now(timezone.utc). Convierte a hora local solo en el punto final, cuando se lo muestras a una persona. Usa el módulo zoneinfo para zonas con nombre como "America/Bogota", que maneja los cambios de horario de verano por ti. Una regla más: nunca midas el tiempo transcurrido con diferencias de datetime.now(), porque el reloj del sistema puede saltar (por una sincronización o un cambio manual); usa time.perf_counter(), un contador que solo avanza hacia adelante, para medir duraciones.

python
from datetime import datetime, date, timedelta

now = datetime.now()             # fecha y hora actuales
today = date.today()             # solo la fecha actual

print(now.year, now.month, now.day)
print(now.hour, now.minute, now.second)

# Formateo
print(now.strftime("%Y-%m-%d"))           # "2024-01-15"
print(now.strftime("%d %B %Y, %H:%M"))   # "15 January 2024, 09:42"

# Parseo
deadline = datetime.strptime("2024-12-31", "%Y-%m-%d")

# Aritmética
tomorrow = today + timedelta(days=1)
next_week = today + timedelta(weeks=1)
diff = deadline - now
print(f"{diff.days} days until deadline")

Códigos comunes de strftime:

CódigoSignificadoEjemplo
%YAño de 4 cifras2024
%mMes (con cero a la izquierda)01
%dDía (con cero a la izquierda)15
%HHora (24h)09
%MMinuto42
%BNombre completo del mesJanuary
Junodatetimedatetime.now() te da la fecha y hora actuales. strftime() lo convierte en una cadena de texto con códigos como %Y-%m-%d, y strptime() hace el camino inverso, parseando una cadena de texto de vuelta. timedelta es una duración que puedes sumar o restar, así que today + timedelta(days=1) es mañana.
Junodatetimedatetime, date, y timedelta son la base. strftime() formatea con códigos, strptime() parsea con un patrón que coincida. Las fechas admiten aritmética: resta dos y obtienes un timedelta, suma un timedelta para desplazarte hacia adelante o hacia atrás.
Junodatetime Un datetime.now() sencillo es naive, sin zona horaria, lo cual se rompe en silencio en cuanto entran en juego dos máquinas distintas. Guarda y transmite en UTC con datetime.now(timezone.utc), convierte a hora local solo para mostrarla, y apóyate en zoneinfo para zonas con nombre. Y mide el tiempo transcurrido con time.perf_counter(), nunca con diferencias de now(), porque el reloj de pared puede saltar.

os y pathlib ​

pathlib es la forma moderna de trabajar con rutas de archivos. Los objetos Path te permiten construir, inspeccionar y navegar rutas usando el operador /. os te da acceso a variables de entorno y operaciones de más bajo nivel del sistema operativo. Prefiere pathlib para código nuevo.

pathlib.Path representa rutas del sistema de archivos como objetos con métodos para consultarlas y navegarlas. El operador / une componentes de la ruta de forma limpia, manejando automáticamente los separadores específicos del sistema operativo. os.environ es un objeto similar a un dict para las variables de entorno; os.environ.get("KEY", "default") es seguro para variables ausentes.

Path se adapta al sistema operativo automáticamente, así que el mismo código une rutas con el separador correcto en cada máquina: escribe Path("data") / "file.csv", nunca cadenas armadas a mano como "data/" + name, que se rompen en Windows. Dos notas prácticas. Primero, .glob(), .rglob(), y .iterdir() devuelven generadores (producen entradas de una en una en lugar de construir una lista completa), así que envuélvelos en list() si necesitas recorrerlos dos veces o conocer la cantidad de antemano; para un directorio enorme, iterar de forma perezosa es justamente lo que se busca. Segundo, algunas bibliotecas más antiguas todavía esperan una cadena de texto simple en lugar de un objeto Path; cuando alguna rechace un Path, pásale str(path) y sigue adelante. Para leer una variable de entorno, usa siempre os.environ.get("KEY", "default") en lugar de os.environ["KEY"], porque la forma indexada lanza KeyError cuando la variable no está definida, que es el caso común en un despliegue recién hecho.

python
from pathlib import Path

p = Path("data/reports")

p.exists()           # True si la ruta existe
p.is_dir()           # True si es un directorio
p.is_file()          # True si es un archivo

p.mkdir(parents=True, exist_ok=True)   # crea los directorios

for f in p.glob("*.csv"):              # todos los archivos CSV del directorio
    print(f.name)                      # el nombre del archivo

report = p / "report_jan.csv"          # el operador / une rutas
report.stem       # "report_jan" (nombre sin extensión)
report.suffix     # ".csv"
report.parent     # Path("data/reports")

content = report.read_text()           # lee el contenido del archivo directamente
report.write_text("new content\n")    # escribe directamente

Para el módulo os:

python
import os

os.getcwd()                        # directorio de trabajo actual
os.listdir(".")                    # lista el contenido del directorio
os.path.exists("data.txt")        # True si la ruta existe
os.path.join("data", "file.txt")  # "data/file.txt" (multiplataforma)
os.environ.get("HOME")            # lee una variable de entorno

Prefiere pathlib para código nuevo. Usa os cuando necesites variables de entorno o trabajar con APIs más antiguas que esperan cadenas de texto.

Junoos y pathlibpathlib.Path trata una ruta de archivo como un objeto que puedes explorar: .exists(), .read_text(), .write_text(), .glob(). El operador / une piezas, así que p / "report.csv" se lee como una ruta real. Recurre a pathlib por sobre el más antiguo os.path en código nuevo.
Junoos y pathlibPath une con / y maneja el separador del sistema operativo por ti, así que las rutas se mantienen portables. os.environ es similar a un dict para las variables de entorno, y os.environ.get("KEY", "default") es la lectura segura, no falla por una clave ausente.
Junoos y pathlib Construye rutas con Path y /, nunca con cadenas pegadas que se rompen en Windows. .glob() y compañía devuelven generadores, así que envuélvelos con list() si necesitas recorrerlos dos veces. Y lee variables de entorno con os.environ.get(key, default): la forma con corchetes lanza KeyError en el momento en que una variable no está definida, que es justo el caso de un despliegue recién hecho.

timeit ​

timeit mide cuánto tarda en ejecutarse un código. Es útil cuando quieres comparar dos enfoques y elegir el más rápido. Ejecuta el código muchas veces para obtener una medición estable.

timeit.timeit(stmt, setup, number) cronometra stmt ejecutándolo number veces y devolviendo el tiempo total transcurrido en segundos. La cadena setup se ejecuta una vez antes del ciclo cronometrado. Divide el resultado por number para obtener el tiempo por llamada. Más repeticiones reducen el ruido causado por la planificación del sistema.

timeit es la herramienta correcta para un microbenchmark: cronometrar un fragmento pequeño de forma aislada, ejecutándolo muchas veces para que el ruido de la planificación se promedie. Silencia ese ruido por ti, pero una sola ejecución sigue siendo poco confiable, así que toma el mejor de varios intentos en lugar de un solo número. El error que hay que evitar es usarlo para encontrar una ralentización en todo un programa: solo cronometra el fragmento que le pasas, así que no puede decirte qué función es realmente lenta. Para eso, usa cProfile, que ejecuta tu programa real e informa a dónde se fue el tiempo. Regla general: timeit para comparar dos implementaciones, cProfile para encontrar el cuello de botella en primer lugar.

python
import timeit

# Cronometra una sola instrucción
timeit.timeit("sum(range(1000))", number=10000)

# Cronometra un bloque más complejo
setup = "data = list(range(1000))"
code = "[x * 2 for x in data]"
time = timeit.timeit(code, setup=setup, number=10000)
print(f"{time:.4f} seconds for 10,000 runs")

number es cuántas veces se repite. Más repeticiones dan una medición más estable.

Junotimeittimeit.timeit() cronometra un fragmento de código ejecutándolo una y otra vez, y luego divides por la cantidad de ejecuciones para obtener un número estable por llamada. Así es como resuelves "cuál de estos dos es más rápido" en lugar de adivinar. Una sola ejecución es demasiado ruidosa como para confiar en ella, así que déjalo repetir.
Junotimeittimeit.timeit(stmt, setup, number) ejecuta stmt esa cantidad de veces y devuelve el total en segundos; la cadena setup se ejecuta una vez antes del ciclo. Divide por number para obtener el tiempo por llamada, y sube las repeticiones para ahogar el ruido de la planificación.
Junotimeittimeit es para microbenchmarks: comparar dos fragmentos pequeños, tomando el mejor de varios en lugar de una sola ejecución ruidosa. No puede decirte qué función de un programa real es lenta, porque solo cronometra lo que le das. Usa cProfile para encontrar el cuello de botella, y luego timeit para comparar soluciones.

string ​

El módulo string ofrece constantes de texto ya construidas para letras, dígitos y puntuación. Útil cuando necesitas verificar caracteres o generar cadenas de texto aleatorias a partir de un alfabeto específico.

Las constantes del módulo string (ascii_letters, digits, punctuation) son cadenas de texto normales que puedes indexar, recorrer, o usar con in. Combinarlas con random.choices() es la forma estándar de generar tokens o contraseñas aleatorias.

Estas constantes son cadenas de texto ordinarias, así que verificar char in string.ascii_letters recorre toda la cadena cada vez. Ese recorrido es O(n): el trabajo crece con la longitud de la cadena, algo aceptable una vez, pero desperdicio en un ciclo ajustado. Si estás verificando pertenencia repetidamente, construye un set una sola vez (letters = set(string.ascii_letters)) y verifica contra eso en su lugar, ya que la búsqueda en un set se mantiene rápida sin importar cuántas entradas tenga. La otra pieza útil aquí es string.Template, que hace una sustitución simple de $name: cuando una plantilla viene de la entrada del usuario o de un archivo de configuración, es más seguro que pasarle texto no confiable a un f-string o a str.format(), porque no puede acceder a tus variables ni llamar métodos.

python
import string

string.ascii_lowercase   # "abcdefghijklmnopqrstuvwxyz"
string.ascii_uppercase   # "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
string.ascii_letters     # ambos combinados
string.digits            # "0123456789"
string.punctuation       # todos los caracteres de puntuación

Útil cuando necesitas verificar caracteres o generar cadenas de texto aleatorias:

python
import string, random

chars = string.ascii_letters + string.digits
password = "".join(random.choices(chars, k=12))
Junostring El módulo string te da conjuntos de caracteres ya armados: string.ascii_letters, string.digits, string.punctuation. Son cadenas de texto normales, así que puedes recorrerlas o tomar muestras de ellas. Útil cada vez que necesitas un alfabeto conocido para construir o verificar contra él.
Junostringstring.ascii_letters, string.digits y compañía son cadenas de texto normales que puedes indexar, recorrer, o probar con in. Combínalas con random.choices() y tienes la receta estándar para un token o contraseña aleatoria. Nota que random no es seguro para secretos reales.
Junostring Las constantes son cadenas de texto normales, así que char in string.ascii_letters recorre todo cada vez que se llama: construye un set una sola vez si estás verificando dentro de un ciclo. Y cuando una plantilla viene de la entrada del usuario, string.Template con $name es más seguro que un f-string, porque no puede acceder a tus variables.

Crear tus propios módulos ​

Cualquier archivo de Python es un módulo. Para usarlo desde otro archivo, impórtalo por el nombre del archivo (sin .py). Puedes importar todo el módulo y usar su contenido con notación de punto, o importar nombres específicos directamente.

Cuando Python importa un módulo, ejecuta el archivo de principio a fin una vez y guarda el resultado en caché en sys.modules. Los imports posteriores del mismo módulo devuelven el objeto en caché sin volver a ejecutar el archivo. Para proyectos más grandes, los módulos se organizan en paquetes: directorios con un archivo __init__.py.

Cuando escribes import utils, Python busca en los directorios de sys.path en orden, empezando por la carpeta del script que ejecutaste. Esta es la fuente del error de import más común del tipo "funciona en mi máquina": ejecuta un archivo desde un directorio de trabajo distinto y la carpeta que espera puede no estar en la ruta, así que el import falla. La solución confiable es ejecutar el proyecto como un paquete (python -m mypackage.main) en lugar de apuntar Python a un archivo suelto. Un paquete es un directorio de módulos con un archivo __init__.py (puede estar vacío; su presencia es lo que le indica a Python que la carpeta es importable). Dentro de un paquete, un import relativo como from . import helpers significa "desde este mismo paquete", lo que mantiene los imports funcionando aunque el paquete se renombre o se mueva. Un detalle delicado: importlib.reload() vuelve a ejecutar un módulo, pero cualquier nombre que ya hayas importado sigue apuntando a la versión antigua, así que recargar es poco confiable para cualquier cosa que no sea un experimento rápido en el REPL.

python
# utils.py
def clamp(value, lo, hi):
    return max(lo, min(value, hi))

PI = 3.14159
python
# main.py
import utils

utils.clamp(150, 0, 100)   # 100
utils.PI                    # 3.14159

from utils import clamp
clamp(50, 0, 100)           # 50

Python encuentra el módulo buscando en el mismo directorio que el archivo que lo importa (y algunos otros lugares). Para proyectos más grandes, los módulos se organizan en paquetes: directorios con un archivo __init__.py.

JunoCrear tus propios módulos Cualquier archivo .py ya es un módulo: impórtalo por su nombre de archivo sin el .py, y sus funciones y variables quedan a tu disposición. import utils y luego utils.clamp(...), o toma un solo nombre con from utils import clamp. Así es como divides un programa que va creciendo en varios archivos.
JunoCrear tus propios módulos Importar un módulo lo ejecuta de principio a fin una vez, y luego lo guarda en caché en sys.modules, así que los imports posteriores devuelven el mismo objeto sin volver a ejecutar el archivo. Por eso, mantén ligero el código de nivel superior; cualquier cosa pesada se ejecuta en el momento del import. Agrupa módulos relacionados en un paquete: una carpeta con un __init__.py.
JunoCrear tus propios módulos Los imports se resuelven a través de sys.path, empezando por la propia carpeta del script, por eso ejecutar un archivo suelto desde el directorio equivocado rompe el import. Ejecútalo como un paquete en su lugar: python -m pkg.main. Y no confíes en importlib.reload() más allá de pruebas rápidas en el REPL, ya que los nombres que ya importaste siguen apuntando a la versión antigua.

__name__ == "__main__" ​

Cuando Python ejecuta un archivo directamente, __name__ se establece en "__main__". Cuando el mismo archivo se importa como módulo, __name__ es el nombre del módulo. Este patrón te permite escribir código que se ejecuta cuando corres el archivo directamente, pero que se omite cuando otro módulo lo importa.

if __name__ == "__main__": es la protección estándar para código de módulo ejecutable. Permite que un módulo sea tanto importable (exponiendo sus funciones) como ejecutable directamente (con código de prueba o demostración). Sin esto, importar el módulo ejecutaría cualquier código de nivel superior, algo que casi nunca se desea.

Python establece __name__ en "__main__" para el archivo que realmente ejecutaste, y en el nombre propio del módulo para cualquier cosa que se importe. Esta protección existe para evitar que los efectos secundarios en tiempo de import (código que hace algo cuando el módulo se carga, como parsear argumentos, abrir una conexión, o ejecutar una demostración) se disparen en el momento en que otro archivo te importa. El patrón que se sostiene bien: pon el trabajo real en una función main() y llámala bajo la protección, así quienes importan el módulo obtienen las funciones sin el arranque, y el archivo igual se ejecuta limpiamente por su cuenta. Omitir la protección es la razón clásica por la que una suite de pruebas misteriosamente ejecuta el código de demostración de alguien en el instante en que importa el módulo.

python
# utils.py
def clamp(value, lo, hi):
    return max(lo, min(value, hi))

if __name__ == "__main__":
    # esto solo se ejecuta cuando corres: python utils.py
    # no cuando corres: import utils
    print(clamp(150, 0, 100))   # 100

Este es un patrón estándar para cualquier módulo que también sea útil como script independiente.

Juno__name__ == '__main__'if __name__ == "__main__": permite que un archivo sea a la vez un módulo importable y un script ejecutable. El código bajo la protección se ejecuta cuando corres python utils.py, y se omite cuando otro archivo hace import utils. Útil para meter una demostración o prueba rápida al final de un módulo.
Juno__name__ == '__main__'__name__ es "__main__" solo en el archivo que ejecutaste directamente. La protección evita que tu código de demostración o prueba se ejecute cuando el módulo se importa, lo cual de otro modo pasaría en silencio. Es la forma estándar de hacer que un archivo funcione tanto como biblioteca como script.
Juno__name__ == '__main__' La protección detiene los efectos secundarios en tiempo de import, parseo de argumentos, demostraciones, conexiones, para que no se disparen cuando alguien te importa. Pon el trabajo en main() y llámala bajo la protección: quienes importan obtienen las funciones, el archivo igual se ejecuta por su cuenta. Omítela y verás una suite de pruebas ejecutar tu demostración en el momento en que importa el módulo.

Lo más destacado de la biblioteca estándar ​

Algunos módulos más que vale la pena conocer. Cada uno resuelve un problema común que implementar por tu cuenta te tomaría bastante trabajo.

La biblioteca estándar es extensa; lo destacado a continuación es lo que encontrarás con más frecuencia en código de producción. Para una referencia completa, docs.python.org/3/library es la fuente autorizada.

La biblioteca estándar es un conjunto curado de módulos bien probados y documentados. Antes de recurrir a un paquete de terceros, revisa si la biblioteca estándar ya tiene una solución: functools, itertools, contextlib, dataclasses, typing, y abc ofrecen cada uno herramientas que los paquetes de terceros a menudo reinventan.

collections: tipos de contenedores especializados:

python
from collections import Counter, defaultdict, deque

Counter(["a", "b", "a", "c", "a"])   # Counter({'a': 3, 'b': 1, 'c': 1})
defaultdict(list)                      # dict que crea automáticamente las claves faltantes
deque([1, 2, 3], maxlen=5)            # append/pop rápido en ambos extremos

itertools: herramientas para trabajar con iterables:

python
import itertools

list(itertools.chain([1, 2], [3, 4]))          # [1, 2, 3, 4]
list(itertools.islice(range(100), 5))          # [0, 1, 2, 3, 4]
list(itertools.combinations([1, 2, 3], 2))     # [(1, 2), (1, 3), (2, 3)]
list(itertools.product([0, 1], repeat=2))      # [(0,0), (0,1), (1,0), (1,1)]

sys: acceso al intérprete de Python:

python
import sys

sys.argv        # lista de argumentos de la línea de comandos
sys.exit(1)     # sale con un código de estado
sys.version     # cadena de texto con la versión de Python

Paquetes de terceros: más allá de la biblioteca estándar, pip instala paquetes de la comunidad:

bash
pip install requests    # biblioteca HTTP
pip install pandas      # manipulación de datos
pip install numpy       # cómputo numérico

Los paquetes de terceros quedan fuera del alcance de esta guía, pero el patrón siempre es el mismo: pip install, y luego import.

En la práctica ​

Combinando random, string, y datetime para generar IDs únicos de partida con marcas de tiempo:

python
import random
import string
from datetime import datetime

def generate_game_id(length: int = 8) -> str:
    chars = string.ascii_uppercase + string.digits
    return "".join(random.choices(chars, k=length))

def timestamp() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

game_id = generate_game_id()
print(f"[{timestamp()}] Starting game {game_id}")

scores = [random.randint(50, 100) for _ in range(5)]
print(f"Round scores: {scores}")
print(f"Best: {max(scores)}")

Usando pathlib y datetime para encontrar archivos en un directorio e informar sus tamaños:

python
from pathlib import Path
from datetime import datetime

def find_files(directory: str, pattern: str = "*.csv") -> list[Path]:
    return sorted(Path(directory).glob(pattern))

def timestamp() -> str:
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

files = find_files(".", "*.md")[:3]
print(f"[{timestamp()}] Found {len(files)} file(s)")
for f in files:
    size = f.stat().st_size if f.exists() else 0
    print(f"  {f.name} ({size} bytes)")

Leyendo la configuración de la app desde variables de entorno con valores por defecto tipados, y escribiendo entradas de log de acceso estructuradas como JSON delimitado por saltos de línea:

python
import os
import json
from datetime import datetime
from pathlib import Path

def load_env_config() -> dict:
    return {
        "debug": os.environ.get("DEBUG", "false").lower() == "true",
        "port": int(os.environ.get("PORT", "8080")),
        "log_level": os.environ.get("LOG_LEVEL", "INFO"),
    }

def write_access_log(method: str, path: str, status: int) -> None:
    log_dir = Path("logs")
    log_dir.mkdir(exist_ok=True)
    entry = {
        "ts": datetime.now().isoformat(),
        "method": method,
        "path": path,
        "status": status,
    }
    with open(log_dir / "access.jsonl", "a") as f:
        f.write(json.dumps(entry) + "\n")

config = load_env_config()
print(f"Starting on port {config['port']}, debug={config['debug']}")
write_access_log("GET", "/users", 200)

El JSON delimitado por saltos de línea (.jsonl) es un formato de log común: cada línea es un objeto JSON válido, lo que hace que sea sencillo transmitirlo, agregarle contenido, y parsearlo línea por línea sin cargar el archivo completo.