Python vs JavaScript Functions
Function definitions, argument handling, closures, and anonymous functions in Python and JavaScript.
Defining Functions
Python has one way to define a named function: def. JavaScript has three: function declarations, function expressions, and arrow functions (=>). This proliferation happened because each form serves a slightly different purpose. Function declarations are hoisted (callable before their source position), function expressions are not, and arrow functions inherit this from their enclosing scope instead of creating their own. Python's def is never hoisted -- you must define before you call. The practical impact is that JavaScript codebases tend to organize helpers at the bottom of a file (relying on hoisting) while Python codebases define helpers before the code that uses them. Arrow functions also omit the function keyword and have an implicit return for single-expression bodies: const double = x => x * 2. Python's closest equivalent, lambda, is restricted to a single expression with no statements, making it useful for short callbacks but unsuitable for multi-line logic. The naming is telling: JavaScript added arrows as everyday tools, while Python kept lambdas deliberately limited to preserve readability. Guido van Rossum has described lambda as a concession he made grudgingly, and Python style guides push developers toward named functions for anything beyond trivial one-liners.
Named function
def greet(name):
return f"Hello, {name}"
print(greet("Ada"))function greet(name) {
return `Hello, ${name}`;
}
console.log(greet("Ada"));Arrow / lambda
double = lambda x: x * 2
print(double(5)) # => 10const double = x => x * 2;
console.log(double(5)); // => 10Can you write this from memory?
Write the function header for `add_item` with one parameter named `item`
Default Arguments
Both languages support default parameter values, but Python has a well-known gotcha: mutable default arguments are evaluated once at function definition time, not per call. Defining def append_to(item, target=[]) and calling it repeatedly mutates the same list object. JavaScript evaluates default values fresh on each call, so function appendTo(item, target = []) behaves as most developers expect. This difference reflects when the default expression is evaluated -- Python at definition, JavaScript at invocation. The workaround in Python is the None-sentinel pattern: def append_to(item, target=None) followed by target = target if target is not None else []. It adds a line of boilerplate but makes the timing explicit. Another default-argument difference: JavaScript allows defaults to reference earlier parameters in the same signature (function area(w, h = w) {}), while Python also supports this since defaults are evaluated left-to-right. However, Python linters flag it as fragile. In practice, both communities rely on defaults extensively for optional configuration, but Python's gotcha means that any tutorial or reference should mention mutable defaults within the first few paragraphs.
Default values
def connect(host, port=5432):
print(f"{host}:{port}")
connect("localhost") # => localhost:5432function connect(host, port = 5432) {
console.log(`${host}:${port}`);
}
connect("localhost"); // => localhost:5432Mutable default gotcha (Python)
def add_item(item, bag=None):
bag = bag if bag is not None else []
bag.append(item)
return bagfunction addItem(item, bag = []) {
bag.push(item);
return bag;
// fresh [] each call -- no gotcha
}Can you write this from memory?
Write the function header for `add_item` with one parameter named `item`
Variadic Arguments: *args/**kwargs vs Rest/Spread
Python's *args collects extra positional arguments into a tuple, and **kwargs collects extra keyword arguments into a dictionary. JavaScript's rest parameter (...args) collects extra arguments into an array. The difference is that Python separates positional from named overflow, while JavaScript lumps everything into one array since it has no built-in keyword argument concept. When calling functions, Python unpacks with * and **, while JavaScript uses the spread operator (...) in both call sites and array/object literals. Spread also works for object merging ({...defaults, ...overrides}), which maps to Python's dictionary unpacking ({**defaults, **overrides}). The conceptual overlap is strong, but the syntax diverges enough to trip up developers switching between the two. Python's keyword-only arguments (placed after *) have no JavaScript equivalent -- destructured options objects serve the same purpose but without enforcement at the language level. JavaScript's approach of passing an options object ({timeout: 5000, retries: 3}) is actually becoming common in Python too, especially in data science libraries, but Python's built-in keyword arguments remain more ergonomic for smaller signatures.
Variadic positional args
def total(*nums):
return sum(nums)
print(total(1, 2, 3)) # => 6function total(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
console.log(total(1, 2, 3)); // => 6Keyword / destructured options
def fetch(url, *, timeout=30, retries=3):
print(f"{url} t={timeout} r={retries}")
fetch("https://api.dev", retries=5)function fetch(url, { timeout = 30, retries = 3 } = {}) {
console.log(`${url} t=${timeout} r=${retries}`);
}
fetch("https://api.dev", { retries: 5 });Can you write this from memory?
Write the function header for `add_item` with one parameter named `item`
Closures
Closures work in both languages: an inner function captures variables from its enclosing scope and retains access even after the outer function returns. The mechanics are nearly identical, but Python adds a restriction. By default, an inner function can read an enclosing variable but cannot reassign it without the nonlocal keyword. Attempting counter += 1 inside a nested function triggers an UnboundLocalError unless you declare nonlocal counter. JavaScript has no such restriction -- let and const in the outer scope are freely readable and writable from the inner function. This Python restriction exists because the language disambiguates reads from writes: reading a variable searches the LEGB scopes, but writing to a name creates a local binding unless you explicitly opt out with global or nonlocal. JavaScript's single-pass scope analysis treats all references to a let variable in the enclosing block as referring to that same binding. The practical impact is small -- you just add one line -- but it trips up Python newcomers who expect closure behavior to match JavaScript. Closures are the backbone of JavaScript module patterns (IIFE, factory functions), while Python's module system and class definitions reduce the need for closure-as-encapsulation patterns.
Counter closure
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
c = make_counter()
print(c(), c()) # => 1 2function makeCounter() {
let count = 0;
return function increment() {
count++;
return count;
};
}
const c = makeCounter();
console.log(c(), c()); // => 1 2