Fundamentos

Funciones en Python: def, return, argumentos, *args y **kwargs, lambda y anotaciones de tipos

Actualizado: · Skillaria.Top

Una función en Python es un bloque de código con nombre que se declara con def, se llama por su nombre con paréntesis y devuelve un resultado con return; si no hay return, devuelve None. Los argumentos se pasan por posición o por nombre, *args reúne los valores posicionales sobrantes en una tupla, **kwargs reúne los argumentos con nombre en un diccionario, y lambda crea una función anónima corta de una sola expresión.

Una función en Python es un fragmento de código con nombre que puede llamarse tantas veces como haga falta con datos de entrada distintos. Se declara con la palabra def, recibe valores mediante parámetros y devuelve el resultado con return. Las funciones son la forma principal de convertir un script largo en pasos claros y comprobables.

Cómo declarar una función: def en Python

La declaración de una función empieza con def, seguido del nombre, los parámetros entre paréntesis y dos puntos. El cuerpo se escribe con una sangría de cuatro espacios: en Python la sangría no es decoración, forma parte de la sintaxis.

def saludar(nombre):
    """Devuelve un saludo para el usuario."""
    return f"¡Hola, {nombre}!"

print(saludar("Ana"))   # ¡Hola, Ana!

La cadena entre comillas triples justo después del encabezado es el docstring, la documentación de la función. La muestra help(saludar) y los consejos del editor. Según la convención PEP 8, los nombres de las funciones se escriben en snake_case: añadir_gasto, obtener_tasa.

La línea def saludar(nombre): se llama firma de la función: el nombre más la lista de parámetros (y las anotaciones, si las hay). Con la firma, quien lee el código entiende cómo llamar a la función sin mirar su cuerpo.

Qué hace return en Python

return termina la función y entrega el valor a quien la llamó. El código que hay después de un return ejecutado no se ejecuta. Si no hay return, o si aparece sin valor, la función devuelve None.

def area(a, b):
    a * b          # el resultado se calcula y se descarta

print(area(3, 4))  # None

def area(a, b):
    return a * b

print(area(3, 4))  # 12

Olvidar el return es uno de los errores más frecuentes de los principiantes: la función «funciona», pero devuelve None y el fallo aparece una línea más abajo. Se pueden devolver varios valores; en realidad es una sola tupla, que es cómodo desempaquetar:

def minimo_maximo(numeros):
    return min(numeros), max(numeros)

bajo, alto = minimo_maximo([4, 1, 9])
print(bajo, alto)   # 1 9

Un return anticipado ayuda a evitar anidamientos innecesarios: primero se descartan los casos especiales y después se escribe la lógica principal.

def descuento(precio, es_socio):
    if precio <= 0:
        return 0
    if not es_socio:
        return precio
    return precio * 0.9

Argumentos de una función en Python: posicionales y con nombre

Los parámetros son los nombres de la declaración de la función; los argumentos son los valores que se pasan al llamarla. Los argumentos se pueden pasar por orden (posicionalmente) o por nombre. Los argumentos con nombre hacen que la llamada se explique sola y que el orden deje de importar.

def añadir_gasto(concepto, importe, categoria="otros"):
    return {"concepto": concepto, "importe": importe, "categoria": categoria}

añadir_gasto("Café", 250)                              # posicional
añadir_gasto(importe=250, concepto="Café")             # por nombre
añadir_gasto("Taxi", 600, categoria="transporte")      # mixto

Un parámetro con valor por defecto es opcional. Las reglas: los parámetros con valor por defecto van después de los obligatorios, y en la llamada los argumentos posicionales van antes que los argumentos con nombre. Si no se cumple, Python da un SyntaxError.

Trampa: el valor por defecto mutable

El valor por defecto se calcula una sola vez, en el momento de ejecutar la línea def, y no en cada llamada. Si es una lista o un diccionario, será compartido por todas las llamadas.

def añadir_elemento(elemento, acumulado=[]):
    acumulado.append(elemento)
    return acumulado

print(añadir_elemento(1))  # [1]
print(añadir_elemento(2))  # [1, 2] — la lista recuerda la llamada anterior

La solución correcta es usar None como valor por defecto y crear el objeto dentro de la función:

def añadir_elemento(elemento, acumulado=None):
    if acumulado is None:
        acumulado = []
    acumulado.append(elemento)
    return acumulado

*args y **kwargs: cualquier número de argumentos

El asterisco delante de un parámetro reúne todos los argumentos posicionales sobrantes en una tupla (tuple), y dos asteriscos reúnen los argumentos con nombre sobrantes en un diccionario (dict). Los nombres args y kwargs son solo una convención; lo que importa son los asteriscos.

def total(*args):
    return sum(args)

print(total(100, 250, 40))   # 390

def crear_usuario(nombre, **kwargs):
    usuario = {"nombre": nombre}
    usuario.update(kwargs)
    return usuario

print(crear_usuario("Ana", ciudad="Valencia", edad=30))
# {'nombre': 'Ana', 'ciudad': 'Valencia', 'edad': 30}

Los mismos asteriscos funcionan en sentido contrario: al llamar, desempaquetan una secuencia o un diccionario en argumentos:

valores = [3, 4]
print(pow(*valores))          # 81

opciones = {"sep": " | ", "end": "\n"}
print("a", "b", **opciones)   # a | b

Parámetros solo posicionales y solo con nombre

La barra / y el asterisco * en la lista de parámetros actúan como barreras: todo lo que va antes de / solo se puede pasar por orden (sintaxis disponible desde Python 3.8, PEP 570), y todo lo que va después de * solo por nombre.

def convertir(importe, /, *, tasa, moneda="USD"):
    return round(importe / tasa, 2), moneda

convertir(100, tasa=90.5)          # (1.1, 'USD')
convertir(importe=100, tasa=90.5)  # TypeError: importe solo se puede pasar por posición
convertir(100, 90.5)               # TypeError: tasa solo se puede pasar por nombre

Los parámetros que solo se pueden usar con nombre evitan confusiones en llamadas como convertir(100, 90.5, "EUR"), donde no está claro qué es cada valor.

Ámbito de las variables: la regla LEGB

Python busca un nombre siguiendo esta cadena: Local (dentro de la función) → Enclosing (en la función externa) → Global (a nivel de módulo) → Built-in (nombres integrados como len). Dentro de una función se puede leer una variable global, pero para asignarle un valor nuevo hace falta la declaración global.

limite = 1000

def supera_limite(importe):
    return importe > limite     # leer una global: correcto

def subir_limite():
    global limite
    limite = 2000             # sin global se crearía una variable local

En la práctica conviene evitar global: una función que cambia el estado externo es difícil de probar. Es mejor recibir el valor como argumento y devolver el resultado.

Funciones anidadas y clausuras

Una función puede declararse dentro de otra. La función interna ve las variables de la externa incluso después de que esta haya terminado; a eso se le llama clausura. Para modificar una variable de ese tipo se usa nonlocal.

def crear_contador():
    cuenta = 0
    def contador():
        nonlocal cuenta
        cuenta += 1
        return cuenta
    return contador

c = crear_contador()
print(c(), c(), c())   # 1 2 3

Las clausuras están en la base de los decoradores y de las fábricas de funciones.

La función también es un objeto

En Python una función es un objeto normal: se puede guardar en una variable, pasar a otra función, devolver desde una función o almacenar en un diccionario. Tiene atributos como __name__, __doc__, __defaults__ y __annotations__.

def doble(x):
    return x * 2

f = doble
print(f(5), f.__name__)                # 10 doble
print(list(map(doble, [1, 2, 3])))    # [2, 4, 6]

Lambda en Python: funciones anónimas

Una función lambda es una función corta sin nombre cuyo cuerpo es una sola expresión. El resultado de la expresión se devuelve automáticamente; no se escribe return.

cuadrado = lambda x: x ** 2
print(cuadrado(4))   # 16

gastos = [("Café", 250), ("Taxi", 600), ("Almuerzo", 450)]
gastos.sort(key=lambda g: g[1], reverse=True)
print(gastos)  # [('Taxi', 600), ('Almuerzo', 450), ('Café', 250)]

print(list(filter(lambda n: n % 2 == 0, range(10))))  # [0, 2, 4, 6, 8]

Las lambdas son útiles como argumento corto para sorted, max, map o filter. Asignar una lambda a una variable, como cuadrado arriba, no lo recomienda PEP 8: si una función necesita nombre, escríbala con def; así tendrá un __name__ claro en el traceback y un lugar para el docstring.

deflambda
Nombresíno (<lambda>)
Cuerpocualquier número de instruccionesuna sola expresión
returnexplícitoimplícito
Docstring, anotacionessíno
Cuándo convienesiempreun key o un callback corto

Anotaciones de tipos: pistas, no comprobaciones

Las anotaciones describen los tipos esperados de los parámetros y del resultado. Python no las comprueba al ejecutar: una función con x: int aceptará sin problemas una cadena. Las comprueban herramientas externas, como mypy, pyright o ty, y el editor, que señala el error antes de ejecutar nada.

def añadir_gasto(concepto: str, importe: float, categoria: str = "otros") -> dict[str, object]:
    return {"concepto": concepto, "importe": importe, "categoria": categoria}

def area(a: float, b: float) -> float:
    a * b   # mypy: Missing return statement

La comprobación se puede lanzar sin instalar nada con uvx mypy .. El ejemplo anterior muestra la utilidad de las anotaciones: un return olvidado con -> float lo detecta el analizador de inmediato.

Desde Python 3.12 (PEP 695), las funciones genéricas se escriben sin TypeVar: el parámetro de tipo va entre corchetes y un alias de tipo se declara con la palabra type.

def primero[T](xs: list[T]) -> T:
    return xs[0]

type Dinero = int | float

En Python 3.14 (PEP 649), las anotaciones pasaron a ser perezosas: se evalúan en el primer acceso, así que se puede hacer referencia a una clase declarada más abajo sin comillas, y from __future__ import annotations ya no hace falta.

«No se pasó» o «se pasó None»: sentinela en 3.15

El valor por defecto None no permite distinguir entre «no se pasó el argumento» y «se pasó None a propósito». Durante años se resolvió con un marcador hecho a mano:

_AUSENTE = object()

def obtener_tasa(moneda, por_defecto=_AUSENTE):
    tasas = {"USD": 90.5}
    if moneda in tasas:
        return tasas[moneda]
    if por_defecto is _AUSENTE:
        raise KeyError(moneda)
    return por_defecto

En Python 3.15 apareció la función integrada sentinel (PEP 661): AUSENTE = sentinel("AUSENTE") crea un marcador único con una representación clara, que al copiarse sigue siendo el mismo, se serializa con pickle y puede usarse en una anotación como float | AUSENTE. En versiones anteriores sigue funcionando el recurso con object().

Errores típicos con funciones

  • Olvidar return: la función devuelve None.
  • Olvidar los paréntesis: print(saludar) muestra el objeto función, no el resultado de la llamada.
  • Valor por defecto mutable: una lista compartida por todas las llamadas.
  • Número incorrecto de argumentos: TypeError: saludar() missing 1 required positional argument: 'nombre'.
  • Nombre que coincide con uno integrado: una función llamada list o sum oculta la función integrada.

Resumen

Las funciones en Python se declaran con def, devuelven el resultado con return y reciben argumentos por posición o por nombre. *args y **kwargs aportan flexibilidad, / y * dan rigor a las llamadas, lambda sirve para funciones de un solo uso y cortas, y las anotaciones de tipos junto con mypy detectan errores antes de ejecutar el programa. En el curso de Python este tema se practica con un registro de gastos dividido en funciones con anotaciones que se comprueban con mypy.

Preguntas frecuentes

¿En qué se diferencia un parámetro de un argumento?

El parámetro es el nombre que aparece en la declaración de la función; el argumento es el valor concreto que se pasa al llamarla. En def saludar(nombre), nombre es un parámetro; en saludar("Ana"), la cadena "Ana" es un argumento.

¿Se puede llamar a una función antes de declararla?

A nivel de módulo, no: la línea def debe ejecutarse antes de la llamada, o aparecerá NameError. Dentro de otra función sí se puede llamar a una función declarada más abajo, siempre que la llamada ocurra después de su declaración.

¿Cuántos valores puede devolver una función?

Formalmente, uno, pero ese valor puede ser una tupla: return a, b devuelve una tupla de dos elementos, que se desempaqueta como x, y = f().

¿Son obligatorias las anotaciones de tipos?

No; Python funciona sin ellas. Pero en el código que leen otras personas, las anotaciones facilitan el mantenimiento y permiten que mypy o pyright detecten errores antes de ejecutar.

¿Se puede usar if dentro de una lambda?

Solo una expresión condicional: lambda x: "par" if x % 2 == 0 else "impar". Las instrucciones, los bucles y las asignaciones no se permiten dentro de una lambda.

Aprende este tema en vídeo

Este tema se explica a fondo y con práctica en el curso «Python 3.15: от новичка до профи».

Ir al curso