Article

Cómo viajan los argumentos en Python

Published May 05, 2026 · 7 hours, 39 minutes ago

Gastón Gaitan
Gastón Gaitan
Backend Engineer · May 05, 2026
Cómo viajan los argumentos en Python
Python · Funciones y argumentos

Cómo se pasan los argumentos a una función en Python

Python no es ni "pass by value" ni "pass by reference" puro. Lo que se pasa es una referencia al objeto. Entender esto explica por qué algunas funciones parecen modificar tus datos y otras no, y es la base de los bugs más comunes de la entrevista técnica.

La pregunta clave

Cuando llamo a una función con un argumento, ¿estoy pasando una copia o el original? Spoiler: pasás una referencia al mismo objeto, pero lo que pase después depende de si ese objeto es mutable.

Inmutables vs mutables

Tipos como int, float, str, tuple son inmutables: no podés modificarlos, solo reasignar. Tipos como list, dict, set son mutables: tienen métodos que cambian el objeto en el lugar.

Regla de oro

Reasignar dentro de la función NO afecta al de afuera. Mutar el objeto SÍ. Esa única frase resuelve el 90% de las dudas sobre paso de argumentos en Python.

El modelo mental: "pass by object reference"

En Python, todas las variables son etiquetas que apuntan a objetos. Cuando llamás a una función, lo que se copia es la etiqueta, no el objeto. Por eso:

  • Si dentro de la función hacés x = otra_cosa, solo movés la etiqueta local. El de afuera no se entera.
  • Si dentro de la función hacés x.append(...), estás tocando el objeto real que también ve el de afuera.

Por eso un int "parece" pasarse por valor (no se puede mutar) y una list "parece" pasarse por referencia (sí se puede mutar). En realidad funcionan igual: lo que cambia es la naturaleza del objeto.

Diferencia clave

Pass by reference vs pasar una referencia al objeto

Suenan igual, no lo son

Mucha gente usa estos dos términos como sinónimos, pero no lo son. La diferencia no está en cómo suena: está en qué cosas te permite modificar la función.

Pass by reference (real)

Lenguajes como C++ con &, o C# con ref. La función recibe la variable original, no una copia.

  • Modificar el contenido del objeto
  • Reasignar la variable original
  • Es como tener acceso directo al "nombre" del caller.

Python (pasar una referencia)

Lo que hace Python: pasa una copia de la referencia al objeto. La función recibe un alias temporal, no la variable.

  • Modificar el objeto (si es mutable)
  • NO podés reasignar la variable original
  • Compartís el objeto, no la variable.
cpp_pass_by_reference.cpp

C++ · Pass by reference real

El & hace que la función reciba la variable misma. Reasignar adentro cambia afuera.

void cambiar(int& x) {
    x = 50;
}

// Despues de llamar cambiar(a), a vale 50 si o si
python_reasignar_no_funciona.py

Python · Reasignar adentro no afecta afuera

Acá se ve que Python NO es pass by reference: la reasignación queda local.

def f(x):
    x = [1, 2, 3]   # reasignacion local

a = [10]
f(a)
print(a)   # -> [10]   (no cambio)
python_mutar_si_funciona.py

Python · Mutar el objeto SÍ funciona

Como compartís el objeto, mutarlo se ve afuera. Pero seguís sin poder reasignar.

def g(x):
    x.append(99)   # mutacion del objeto

a = [10]
g(a)
print(a)   # -> [10, 99]
Frase corta (nivel entrevista): "En pass by reference se comparte la variable; en Python se comparte el objeto a través de una copia de la referencia, lo que permite mutar pero no reasignar."
Inmutables

int, str, tuple → reasignar no muta

No te asustes

Con tipos inmutables, cualquier "modificación" dentro de la función crea un objeto nuevo y reapunta la variable local. El de afuera queda intacto.

int_no_se_muta.py

Ejemplo 1 · Un int adentro de una función

Reasignar x dentro de la función no toca al de afuera.

def f(x):
    x = x + 1

a = 5
f(a)
print(a)   # -> 5  (no cambia)
lista_reasignada.py

Ejemplo 2 · Reasignar una lista (no la mutás)

Aunque sea mutable, si reasignás en vez de mutar, el de afuera no se entera.

def f(lst):
    lst = lst + [4]   # crea una NUEVA lista, reapunta local

a = [1, 2, 3]
f(a)
print(a)   # -> [1, 2, 3]
Mutables

list, dict, set → mutar SÍ afecta afuera

Side effect

Si dentro de la función llamás un método que muta el objeto (.append, .extend, .pop, d[k] = v), el cambio queda. La variable de afuera apunta al mismo objeto.

mutar_modifica.py

Ejemplo 1 · append dentro de la función

Acá no reasignamos, mutamos el objeto. El cambio se ve afuera.

def agregar_modificando(lista, valor):
    lista.append(valor)

a = [1, 2]
agregar_modificando(a, 10)
print(a)   # -> [1, 2, 10]
no_mutar.py

Ejemplo 2 · Versión "pura" (sin side effects)

Si querés evitar side effects, devolvé una copia nueva.

def agregar_sin_modificar(lista, valor):
    return lista + [valor]   # NUEVA lista

# Variante mas explicita:
def agregar_sin_modificar_v2(lista, valor):
    nueva = lista.copy()
    nueva.append(valor)
    return nueva

a = [1, 2]
b = agregar_sin_modificar(a, 10)
print(a)   # -> [1, 2]
print(b)   # -> [1, 2, 10]
Trampa típica: lista.append(x) devuelve None. Si escribís lista = lista.append(x) tu variable termina valiendo None.
Bug clásico

Default arguments mutables

Pregunta de entrevista

Los valores por defecto se evalúan una sola vez, cuando se define la función, y se reutilizan en cada llamada. Si el default es mutable (una lista, un dict), todas las llamadas comparten el mismo objeto.

default_mutable.py

El bug

La lista por defecto NO se reinicia en cada llamada: se acumula.

def agregar_elemento(lista=[]):
    lista.append(1)
    return lista

print(agregar_elemento())   # -> [1]
print(agregar_elemento())   # -> [1, 1]   (!)
print(agregar_elemento())   # -> [1, 1, 1]
default_correcto.py

La forma correcta

Usá None como sentinela y creá la lista adentro.

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

print(agregar_elemento())   # -> [1]
print(agregar_elemento())   # -> [1]
print(agregar_elemento())   # -> [1]
Reglón de oro: nunca uses [], {} o set() como valor por defecto. Usá None y resolvé adentro.
Aliasing

Dos nombres, un solo objeto

a = b

Cuando hacés b = a no copiás la lista, copiás la etiqueta. Las dos variables apuntan al mismo objeto. Esto es exactamente lo mismo que pasa al pasar un argumento a una función.

alias_mutar.py

Mutar a través de un alias

Si mutás a, b "ve" el cambio porque es el mismo objeto.

a = [1, 2]
b = a

a.append(3)

print(a)   # -> [1, 2, 3]
print(b)   # -> [1, 2, 3]   (mismo objeto)
alias_trampa.py

El caso trampa con +=

Sobre listas, += muta el objeto. Por eso b también ve el cambio.

a = [1, 2]
b = a

a += [3]   # equivalente a a.extend([3]) -> MUTA

print(a)   # -> [1, 2, 3]
print(b)   # -> [1, 2, 3]

# Compara con esto:
a = [1, 2]
b = a
a = a + [3]   # crea NUEVA lista, reasigna a
print(a)   # -> [1, 2, 3]
print(b)   # -> [1, 2]   (b sigue apuntando al original)
Operaciones

+ vs += vs append vs extend

Tabla resumen

Todas "agregan cosas" a una lista, pero algunas crean un objeto nuevo y otras mutan el existente. Esto importa muchísimo cuando pasás listas a funciones.

Operación ¿Muta? ¿Crea objeto nuevo? Comportamiento
a + [x] No Devuelve una lista nueva, no toca a.
a += [x] No Equivalente a a.extend([x]). Muta a in-place.
a.append(x) No Agrega un solo elemento al final. Devuelve None.
a.extend([x, y]) No Agrega cada elemento del iterable. Devuelve None.
append_vs_extend.py

append vs extend

append mete el argumento como un único elemento. extend desarma el iterable.

a = [1, 2]
a.append([3, 4])
print(a)   # -> [1, 2, [3, 4]]

b = [1, 2]
b.extend([3, 4])
print(b)   # -> [1, 2, 3, 4]

Pregunta de entrevista que filtra gente

Combina default mutable + mutación. ¿Qué imprime?

def f(x, lst=[]):
    lst.append(x)
    return lst

print(f(1))   # -> [1]
print(f(2))   # -> [1, 2]
print(f(3))   # -> [1, 2, 3]

¿Por qué? El default lst=[] se evalúa una vez al definir la función, y todas las llamadas comparten esa misma lista. Cada append muta ese único objeto.

Cierre rápido

Cinco frases para guardar en la cabeza cuando pasás argumentos a funciones en Python:

  • Modelo Pass by object reference. Se copia la etiqueta, no el objeto.
  • Inmutables int, str, tuple. Reasignar adentro no afecta afuera.
  • Mutables list, dict, set. Mutar adentro SÍ afecta afuera.
  • Defaults Nunca uses [] o {} como default. Usá None.
  • Operadores + crea, += muta. append/extend siempre mutan y devuelven None.

Conclusión

No existe en Python "paso por valor" ni "paso por referencia" en el sentido clásico de C++ o Java. Lo único que tenés que recordar es qué tipo de objeto estás pasando y qué le hacés adentro: si lo reasignás, el cambio queda en la función; si lo mutás, escapa.

El día que tengas claro esto, dejás de tener bugs raros con listas, defaults y "¿por qué se modificó esto si no lo toqué?". Reasignar es local. Mutar es global. Esa es la regla.

Para seguir leyendo

Si querés profundizar todavía más en cómo Python maneja los argumentos (y ver decenas de ejemplos discutidos por la comunidad), te recomiendo este hilo clásico de Stack Overflow:

How do I pass a variable by reference? — Stack Overflow