Software Crafters® 2025 | Creado con 🖤 para elevar el nivel de la conversación sobre programación en español | Legal
Antes de empezar tengo que advertirte que ningún lenguaje de programación, por simple que sea, puede aprenderse en profundidad en tan poco tiempo, a no ser que se requiera de experiencia previa en otros lenguajes. Dominar la programación precisa de experiencia, lo cual a su vez requiere de un tiempo mínimo que permita afianzar las estructuras mentales necesarias para entender la secuencia lógica a seguir para desarrollar un programa o proyecto de software.
El objetivo de este artículo no es enseñar a programar, sino exponer en forma concisa los elementos más importantes del lenguaje Python 3.12+, sería algo así como una mezcla entre un tutorial y una cheatsheet. Incluyo ejemplos de código prácticos y ejecutables para cada concepto, con las características más modernas del lenguaje.
Python es un lenguaje multiparadigma, que soporta orientación a objetos, programación imperativa y programación funcional. Es interpretado, de tipado dinámico (aunque soporta type hints) y multiplataforma.
Para seguir este tutorial necesitas Python 3.10 o superior. Puedes descargarlo desde python.org.
Verifica tu instalación:
python3 --version # Python 3.12.0 (o superior)
Para ejecutar código Python puedes usar:
python3 en la terminal.py: python3 mi_script.pyLa sintaxis de Python es extremadamente "limpia". No se requiere ningún carácter que indique el final de una sentencia y los bloques se definen a través de la indentación del código.
# Esto es un comentario de una línea """ Esto es un comentario multilínea. También se usa para docstrings. """ # Los bloques se definen por indentación (4 espacios) if True: print("Este código está indentado") print("Pertenece al bloque if")
# Operaciones básicas suma = 10 + 5 # 15 resta = 10 - 5 # 5 multiplicacion = 10 * 5 # 50 division = 10 / 3 # 3.333... (siempre retorna float) division_entera = 10 // 3 # 3 modulo = 10 % 3 # 1 potencia = 2 ** 3 # 8 # Operadores de asignación compuesta x = 10 x += 5 # x = x + 5 → 15 x -= 3 # x = x - 3 → 12 x *= 2 # x = x * 2 → 24 x /= 4 # x = x / 4 → 6.0
# Operadores de comparación igual = 5 == 5 # True diferente = 5 != 3 # True mayor = 5 > 3 # True menor = 3 < 5 # True mayor_igual = 5 >= 5 # True menor_igual = 3 <= 5 # True # Operadores lógicos verdadero = True and True # True falso = True and False # False cierto = True or False # True negacion = not True # False # Encadenamiento de comparaciones x = 5 es_valido = 1 < x < 10 # True (equivalente a: 1 < x and x < 10)
Los tipos de datos básicos en Python son numéricos (enteros, reales, complejos), cadenas de texto, booleanos y None (nulo). La función
type() nos devuelve el tipo:
# Números enteros edad = 30 print(type(edad)) # <class 'int'> # Números flotantes altura = 1.75 print(type(altura)) # <class 'float'> # Números complejos complejo = 3 + 4j print(type(complejo)) # <class 'complex'> # Cadenas de texto nombre = "Python" apellido = 'Developer' print(type(nombre)) # <class 'str'> # Booleanos activo = True desactivado = False print(type(activo)) # <class 'bool'> # None (ausencia de valor) vacio = None print(type(vacio)) # <class 'NoneType'> # Conversión entre tipos numero_str = "42" numero_int = int(numero_str) # 42 numero_float = float(numero_str) # 42.0 texto = str(100) # "100"
Python soporta anotaciones de tipo que mejoran la legibilidad y permiten detección de errores:
# Variables con type hints nombre: str = "Miguel" edad: int = 30 activo: bool = True # Union types con | (Python 3.10+) valor: int | float = 42 resultado: str | None = None # Listas y diccionarios con tipos numeros: list[int] = [1, 2, 3] usuarios: dict[str, int] = {"Alice": 25, "Bob": 30}
edad = 18 if edad < 18: print("Eres menor de edad") elif edad == 18: print("Acabas de ser mayor de edad") else: print("Eres mayor de edad") # Expresiones ternarias mensaje = "Mayor" if edad >= 18 else "Menor" # Operador walrus := (Python 3.8+) # Asigna y evalúa en la misma expresión if (n := len([1, 2, 3])) > 2: print(f"La lista tiene {n} elementos")
Pattern matching estructural, similar a switch en otros lenguajes pero más poderoso:
def procesar_comando(comando): match comando: case "salir" | "exit" | "quit": return "Saliendo..." case "ayuda" | "help": return "Comandos disponibles: salir, ayuda, estado" case ["mover", direccion]: return f"Moviendo hacia {direccion}" case {"tipo": "usuario", "nombre": nombre}: return f"Hola, {nombre}" case _: return "Comando no reconocido" print(procesar_comando("ayuda")) # Comandos disponibles... print(procesar_comando(["mover", "norte"])) # Moviendo hacia norte
# For con range for i in range(5): print(i) # 0, 1, 2, 3, 4 # For con lista frutas = ["manzana", "banana", "naranja"] for fruta in frutas: print(fruta) # For con enumerate (índice y valor) for indice, fruta in enumerate(frutas): print(f"{indice}: {fruta}") # For con diccionario precios = {"manzana": 1.5, "banana": 0.8} for fruta, precio in precios.items(): print(f"{fruta}: ${precio}") # While contador = 0 while contador < 5: print(contador) contador += 1 # Break y continue for i in range(10): if i == 3: continue # Salta a la siguiente iteración if i == 7: break # Sale del bucle print(i)
Una función es un bloque de código reutilizable. En Python moderno usamos type hints para mayor claridad:
# Función básica def saludar(): print("¡Hola!") saludar() # ¡Hola! # Función con parámetros y retorno def sumar(a: int, b: int) -> int: return a + b resultado = sumar(5, 3) # 8 # Parámetros con valores por defecto def saludar_persona(nombre: str, saludo: str = "Hola") -> str: return f"{saludo}, {nombre}!" print(saludar_persona("Ana")) # Hola, Ana! print(saludar_persona("Ana", "Buenos días")) # Buenos días, Ana! # Múltiples valores de retorno def dividir(a: int, b: int) -> tuple[int, int]: cociente = a // b resto = a % b return cociente, resto q, r = dividir(10, 3) # q=3, r=1 # Funciones lambda (anónimas) cuadrado = lambda x: x ** 2 print(cuadrado(5)) # 25 # Lambda con múltiples parámetros suma = lambda a, b: a + b print(suma(3, 4)) # 7
# *args: argumentos posicionales variables (tupla) def sumar_todos(*args: int) -> int: """Suma todos los argumentos recibidos""" return sum(args) print(sumar_todos(1, 2, 3)) # 6 print(sumar_todos(1, 2, 3, 4, 5)) # 15 # **kwargs: argumentos con nombre variables (diccionario) def mostrar_info(**kwargs: str) -> None: """Muestra información en formato clave: valor""" for clave, valor in kwargs.items(): print(f"{clave}: {valor}") mostrar_info(nombre="Ana", edad="25", ciudad="Madrid") # nombre: Ana # edad: 25 # ciudad: Madrid # Combinando todo def funcion_completa( obligatorio: str, opcional: str = "default", *args: int, **kwargs: str ) -> None: print(f"Obligatorio: {obligatorio}") print(f"Opcional: {opcional}") print(f"Args: {args}") print(f"Kwargs: {kwargs}") funcion_completa("requerido", "custom", 1, 2, 3, extra="valor")
Las cadenas en Python son inmutables y muy versátiles:
# Definición de cadenas simple = 'Hola' doble = "Mundo" multilinea = """ Este es un texto de múltiples líneas """ # F-strings (Python 3.6+) - Forma recomendada nombre = "Python" version = 3.12 mensaje = f"Estoy usando {nombre} {version}" # F-strings con expresiones x = 10 print(f"El doble de {x} es {x * 2}") # F-strings con formato precio = 19.99 print(f"Precio: ${precio:.2f}") # Precio: $19.99 # F-string para debugging (Python 3.8+) variable = 42 print(f"{variable=}") # variable=42 # Operaciones con cadenas texto = "python" print(texto.upper()) # PYTHON print(texto.capitalize()) # Python print(texto.title()) # Python # Búsqueda frase = "Python es genial" print("Python" in frase) # True print(frase.startswith("Python")) # True print(frase.endswith("genial")) # True print(frase.find("es")) # 7 (índice donde empieza) # División y unión palabras = "uno,dos,tres".split(",") # ['uno', 'dos', 'tres'] unido = "-".join(palabras) # uno-dos-tres # Reemplazo texto = "Hola mundo" nuevo = texto.replace("mundo", "Python") # Hola Python # Limpieza espacios = " texto con espacios " print(espacios.strip()) # "texto con espacios" # Slicing (subcadenas) texto = "Python" print(texto[0]) # P print(texto[-1]) # n print(texto[0:3]) # Pyt print(texto[::2]) # Pto (cada 2 caracteres) print(texto[::-1]) # nohtyP (invertido) # Raw strings (útil para regex) ruta = r"C:\nueva\carpeta" # No interpreta \n como nueva línea
Las listas son colecciones ordenadas y mutables:
# Creación numeros = [1, 2, 3, 4, 5] mixta = [1, "dos", 3.0, True] vacia = [] # Acceso print(numeros[0]) # 1 (primer elemento) print(numeros[-1]) # 5 (último elemento) # Modificación numeros[0] = 10 print(numeros) # [10, 2, 3, 4, 5] # Métodos comunes numeros.append(6) # Agregar al final numeros.insert(0, 0) # Insertar en posición numeros.remove(10) # Eliminar valor elemento = numeros.pop() # Eliminar y retornar último numeros.extend([7, 8]) # Extender con otra lista # Operaciones longitud = len(numeros) hay_tres = 3 in numeros maximo = max(numeros) minimo = min(numeros) suma = sum(numeros) # Slicing sublista = numeros[1:4] # Elementos 1, 2, 3 invertida = numeros[::-1] # Lista invertida # List comprehensions cuadrados = [x ** 2 for x in range(5)] # [0, 1, 4, 9, 16] pares = [x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8] # Con if-else en comprehension valores = [x if x > 0 else 0 for x in [-1, 2, -3, 4]] # [0, 2, 0, 4] # Ordenamiento numeros.sort() # Ordena in-place ordenados = sorted(numeros) # Retorna nueva lista ordenada descendente = sorted(numeros, reverse=True)
Las tuplas son como listas pero inmutables (no se pueden modificar):
# Creación coordenadas = (10, 20) colores = ("rojo", "verde", "azul") singleton = (42,) # Nota la coma para tupla de 1 elemento # Acceso (igual que listas) x, y = coordenadas # Desempaquetado print(coordenadas[0]) # 10 # Las tuplas son inmutables # coordenadas[0] = 15 # ¡ERROR! TypeError # Uso común: retorno de múltiples valores def obtener_dimensiones(): return 1920, 1080 ancho, alto = obtener_dimensiones() # Named tuples (más expresivas) from collections import namedtuple Punto = namedtuple('Punto', ['x', 'y']) p = Punto(10, 20) print(p.x, p.y) # 10 20
Los diccionarios almacenan pares clave-valor:
# Creación persona = { "nombre": "Ana", "edad": 30, "ciudad": "Madrid" } # Otra forma persona = dict(nombre="Ana", edad=30, ciudad="Madrid") # Acceso print(persona["nombre"]) # Ana print(persona.get("edad")) # 30 print(persona.get("telefono", "No disponible")) # Valor por defecto # Modificación persona["edad"] = 31 # Actualizar persona["email"] = "ana@example.com" # Agregar nueva clave del persona["ciudad"] # Eliminar # Métodos claves = persona.keys() valores = persona.values() items = persona.items() # Pares (clave, valor) # Verificación existe = "nombre" in persona # True # Iteración for clave in persona: print(f"{clave}: {persona[clave]}") for clave, valor in persona.items(): print(f"{clave}: {valor}") # Dictionary comprehension cuadrados = {x: x**2 for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16} # Filtrado mayores = {k: v for k, v in persona.items() if isinstance(v, int)} # Merge de diccionarios (Python 3.9+) dict1 = {"a": 1, "b": 2} dict2 = {"c": 3, "d": 4} combinado = dict1 | dict2 # {'a': 1, 'b': 2, 'c': 3, 'd': 4}
Los sets son colecciones no ordenadas de elementos únicos:
# Creación numeros = {1, 2, 3, 4, 5} letras = set("hello") # {'h', 'e', 'l', 'o'} - duplicados eliminados # Sets vacíos (no usar {} que crea diccionario) vacio = set() # Operaciones numeros.add(6) numeros.remove(1) # Error si no existe numeros.discard(10) # No error si no existe # Operaciones de conjuntos a = {1, 2, 3, 4} b = {3, 4, 5, 6} union = a | b # {1, 2, 3, 4, 5, 6} interseccion = a & b # {3, 4} diferencia = a - b # {1, 2} simetrica = a ^ b # {1, 2, 5, 6} # Verificaciones es_subconjunto = {1, 2} <= a # True es_superconjunto = a >= {1, 2} # True # Set comprehension pares = {x for x in range(10) if x % 2 == 0} # Uso común: eliminar duplicados lista = [1, 2, 2, 3, 3, 3, 4] unicos = list(set(lista)) # [1, 2, 3, 4]
class Persona: """Representa una persona con nombre y edad""" # Variable de clase (compartida por todas las instancias) especie = "Homo sapiens" def __init__(self, nombre: str, edad: int): """Constructor de la clase""" self.nombre = nombre # Atributo de instancia self.edad = edad def saludar(self) -> str: """Método de instancia""" return f"Hola, soy {self.nombre}" def cumplir_anos(self) -> None: """Incrementa la edad en 1""" self.edad += 1 def __str__(self) -> str: """Representación en string""" return f"Persona(nombre={self.nombre}, edad={self.edad})" def __repr__(self) -> str: """Representación para debugging""" return f"Persona('{self.nombre}', {self.edad})" # Uso persona = Persona("Ana", 25) print(persona.saludar()) # Hola, soy Ana persona.cumplir_anos() print(persona.edad) # 26 print(persona) # Persona(nombre=Ana, edad=26)
class Empleado(Persona): """Hereda de Persona y añade salario""" def __init__(self, nombre: str, edad: int, salario: float): super().__init__(nombre, edad) # Llama al constructor de Persona self.salario = salario def saludar(self) -> str: """Sobrescribe el método saludar""" return f"Hola, soy {self.nombre} y soy empleado" def aumentar_salario(self, porcentaje: float) -> None: self.salario *= (1 + porcentaje / 100) empleado = Empleado("Carlos", 30, 50000) print(empleado.saludar()) # Hola, soy Carlos y soy empleado empleado.aumentar_salario(10) print(f"Nuevo salario: ${empleado.salario}") # $55000.0
Forma moderna y concisa de crear clases para datos:
from dataclasses import dataclass, field @dataclass class Producto: """Clase para representar un producto""" nombre: str precio: float cantidad: int = 0 # Valor por defecto etiquetas: list[str] = field(default_factory=list) def valor_total(self) -> float: """Calcula el valor total del inventario""" return self.precio * self.cantidad def __post_init__(self): """Se ejecuta después de __init__""" if self.precio < 0: raise ValueError("El precio no puede ser negativo") # Uso producto = Producto("Laptop", 999.99, 5, ["electrónica", "computadoras"]) print(producto) # Producto(nombre='Laptop', precio=999.99, ...) print(producto.valor_total()) # 4999.95 # Las dataclasses automáticamente generan: # - __init__ # - __repr__ # - __eq__ # - Y más dependiendo de los parámetros
class Rectangulo: """Rectángulo con propiedades calculadas""" def __init__(self, ancho: float, alto: float): self._ancho = ancho self._alto = alto @property def area(self) -> float: """Propiedad de solo lectura""" return self._ancho * self._alto @property def ancho(self) -> float: return self._ancho @ancho.setter def ancho(self, valor: float) -> None: if valor <= 0: raise ValueError("El ancho debe ser positivo") self._ancho = valor def __str__(self) -> str: return f"Rectángulo({self._ancho}x{self._alto})" rect = Rectangulo(5, 3) print(rect.area) # 15 (calculado automáticamente) rect.ancho = 10 print(rect.area) # 30
def procesar_dato(dato): """Ejemplos avanzados de pattern matching""" match dato: # Match literal case 0: return "Cero" # Match con condición (guard) case x if x < 0: return f"Negativo: {x}" # Match de tuplas case (x, y): return f"Par: ({x}, {y})" # Match de listas con captura case [first, *rest]: return f"Primero: {first}, Resto: {rest}" # Match de diccionarios case {"nombre": nombre, "edad": edad}: return f"{nombre} tiene {edad} años" # Match de clases case Persona(nombre=n, edad=e) if e >= 18: return f"{n} es mayor de edad" # Wildcard case _: return "Otro tipo de dato" print(procesar_dato([1, 2, 3, 4])) # Primero: 1, Resto: [2, 3, 4]
# Antes (Python 3.9-) from typing import Union, Optional def procesar(valor: Union[int, str]) -> Optional[str]: pass # Ahora (Python 3.10+) def procesar(valor: int | str) -> str | None: if isinstance(valor, int): return str(valor) return valor # Múltiples opciones ResultadoAPI = dict[str, str] | list[str] | None
# Genéricos simplificados en Python 3.12+ def primero[T](items: list[T]) -> T | None: """Retorna el primer elemento o None""" return items[0] if items else None # Clases genéricas class Caja[T]: """Contenedor genérico""" def __init__(self, contenido: T): self.contenido = contenido def obtener(self) -> T: return self.contenido caja_int = Caja(42) caja_str = Caja("Hola")
# Debugging con = x, y = 10, 20 print(f"{x=}, {y=}, {x+y=}") # x=10, y=20, x+y=30 # Formato de fechas from datetime import datetime ahora = datetime.now() print(f"{ahora:%Y-%m-%d %H:%M:%S}") # Alineación y relleno nombre = "Python" print(f"{nombre:>10}") # Alineado a derecha print(f"{nombre:^10}") # Centrado print(f"{nombre:<10}") # Alineado a izquierda # Números pi = 3.14159265359 print(f"{pi:.2f}") # 3.14 print(f"{1000000:,}") # 1,000,000
# Asignar y usar en la misma expresión # Útil en comprensiones y condicionales # En condicionales if (n := len([1, 2, 3, 4, 5])) > 3: print(f"Lista larga con {n} elementos") # En while import random while (numero := random.randint(1, 10)) != 5: print(f"Intentando... {numero}") print("¡Encontrado el 5!") # En comprensiones datos = [1, 2, 3, 4, 5] resultado = [(x, cuadrado) for x in datos if (cuadrado := x**2) > 10] # [(4, 16), (5, 25)]
# Agrupar múltiples excepciones try: raise ExceptionGroup("Errores múltiples", [ ValueError("Valor inválido"), TypeError("Tipo incorrecto"), ]) except* ValueError as e: print(f"Manejando ValueError: {e}") except* TypeError as e: print(f"Manejando TypeError: {e}")
Las excepciones permiten manejar errores de forma controlada:
# Try-except básico try: resultado = 10 / 0 except ZeroDivisionError: print("No se puede dividir por cero") # Múltiples excepciones try: valor = int("abc") except ValueError: print("Error de conversión") except TypeError: print("Error de tipo") # Capturar múltiples tipos try: # código que puede fallar pass except (ValueError, TypeError) as e: print(f"Error: {e}") # Try-except-else-finally try: archivo = open("datos.txt", "r") contenido = archivo.read() except FileNotFoundError: print("Archivo no encontrado") else: # Se ejecuta si no hubo excepciones print(f"Archivo leído: {len(contenido)} caracteres") finally: # Siempre se ejecuta if 'archivo' in locals(): archivo.close() # Context managers (recomendado para archivos) try: with open("datos.txt", "r") as archivo: contenido = archivo.read() except FileNotFoundError: print("Archivo no encontrado") # Lanzar excepciones def validar_edad(edad: int) -> None: if edad < 0: raise ValueError("La edad no puede ser negativa") if edad > 150: raise ValueError("Edad no realista") # Excepciones personalizadas class ErrorValidacion(Exception): """Excepción personalizada para errores de validación""" pass class ErrorEdadInvalida(ErrorValidacion): """Error cuando la edad no es válida""" def __init__(self, edad: int, mensaje: str = "Edad inválida"): self.edad = edad self.mensaje = f"{mensaje}: {edad}" super().__init__(self.mensaje) try: raise ErrorEdadInvalida(-5) except ErrorEdadInvalida as e: print(e.mensaje) # Edad inválida: -5
Python 3.7+ incluye la función
breakpoint() que es más flexible que pdb.set_trace():
def calcular_factorial(n: int) -> int: resultado = 1 for i in range(1, n + 1): resultado *= i breakpoint() # El depurador se detendrá aquí return resultado # Al ejecutar, se abre el debugger interactivo # Comandos básicos de pdb: # n (next): siguiente línea # s (step): entrar en función # c (continue): continuar ejecución # p variable: imprimir variable # l (list): mostrar código # q (quit): salir del debugger
# ipdb: Mejor que pdb con autocompletado y colores # pip install ipdb import ipdb ipdb.set_trace() # pudb: Debugger visual en terminal # pip install pudb import pudb pudb.set_trace() # Logging en lugar de prints import logging logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) logger.debug("Mensaje de debug") logger.info("Información general") logger.warning("Advertencia") logger.error("Error") logger.critical("Error crítico") # Assert para validaciones en desarrollo def calcular_promedio(numeros: list[float]) -> float: assert len(numeros) > 0, "La lista no puede estar vacía" return sum(numeros) / len(numeros)
Python tiene convenciones de estilo documentadas en PEP 8:
# Naming conventions variable_nombre = "snake_case para variables y funciones" CONSTANTE = "MAYÚSCULAS para constantes" class MiClase: # PascalCase para clases pass # Indentación: 4 espacios def funcion(): if True: print("4 espacios por nivel") # Longitud de línea: 79 caracteres (88 para Black) texto_largo = ( "Para líneas largas, usar paréntesis " "para continuar en la siguiente línea" ) # Imports # 1. Librería estándar import os import sys # 2. Librerías de terceros import numpy as np import pandas as pd # 3. Imports locales from mi_modulo import mi_funcion # Espaciado x = 5 # Espacio alrededor de operadores lista = [1, 2, 3] # Espacio después de comas funcion(a, b, c) # No espacio antes de paréntesis # Comparaciones # Bien if variable is None: pass if valor in lista: pass # Mal if variable == None: # Usar 'is' para None pass # Type hints def funcion(param: str, otro: int = 0) -> bool: return True # Docstrings def mi_funcion(x: int) -> int: """ Descripción breve de la función. Args: x: Descripción del parámetro Returns: Descripción del valor de retorno Raises: ValueError: Cuándo se lanza esta excepción """ return x * 2
# Black: Formateador automático pip install black black mi_script.py # Ruff: Linter moderno y rápido (reemplaza flake8, pylint, etc.) pip install ruff ruff check mi_script.py ruff format mi_script.py # mypy: Verificación de tipos estáticos pip install mypy mypy mi_script.py # isort: Ordenar imports automáticamente pip install isort isort mi_script.py
Gestión de entornos:
venv: Incluido en Python, para entornos virtualespoetry: Gestión moderna de dependenciaspyenv: Gestión de múltiples versiones de PythonDesarrollo:
Testing:
pytest: Framework de testing modernounittest: Incluido en la librería estándarhypothesis: Property-based testingCalidad de código:
black: Formateador automáticoruff: Linter ultrarrápidomypy: Type checkerpre-commit: Git hooks para calidadEste tutorial ha cubierto los fundamentos de Python 3.12+ con ejemplos prácticos de:
Python es un lenguaje en constante evolución. Mantente actualizado con las nuevas versiones y sigue practicando para convertirte en un Pythonista experto.
Si te ha gustado el artículo, valora y comparte en tus redes sociales.
¿Quiéres leer más artículos como éste? Pues suscríbete a la newsletter