Covers default arguments, keyword arguments, *args, **kwargs, and unpacking. Short and dense — worth reading after this lesson.
The core syntax is similar to JS — just different keywords and indentation.
function add(a, b) {
return a + b;
}
// Arrow function
const add = (a, b) => a + b;
// Default params
function greet(name = "World") {
return `Hello, ${name}!`;
}
// No explicit return = undefined
function nothing() {}
def add(a, b):
return a + b
# Lambda (limited to one expression)
add = lambda a, b: a + b
# Default params
def greet(name="World"):
return f"Hello, {name}!"
# No explicit return = None
def nothing():
pass # 'pass' is required for empty blocks
Never use a mutable type (list, dict) as a default argument. It's shared across all calls:
# BAD — the list is created ONCE and reused
def append_to(item, lst=[]):
lst.append(item)
return lst
append_to(1) # [1]
append_to(2) # [1, 2] ← surprise!
# GOOD — use None as sentinel
def append_to(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
These are Python's equivalent of JS's ...rest — but more powerful because they work for both positional and keyword arguments.
# *args — captures extra positional args as a tuple
def log(*args):
for item in args:
print(item)
log("error", "file not found", 404)
# "error"
# "file not found"
# 404
# **kwargs — captures extra keyword args as a dict
def create_user(**kwargs):
print(kwargs)
create_user(name="Alice", age=30, admin=True)
# {"name": "Alice", "age": 30, "admin": True}
# Combining both
def mixed(required, *args, **kwargs):
print(required, args, kwargs)
mixed("hi", 1, 2, x=10, y=20)
# "hi" (1, 2) {"x": 10, "y": 20}
def add(a, b, c):
return a + b + c
nums = [1, 2, 3]
add(*nums) # unpacks list → add(1, 2, 3)
config = {"a": 1, "b": 2, "c": 3}
add(**config) # unpacks dict → add(a=1, b=2, c=3)
Python lets you force callers to use argument names — no equivalent in JS.
# After the *, all args MUST be passed by name
def send_email(to, *, subject, body):
pass
send_email("alice@x.com", subject="Hi", body="Hello") # ✅
send_email("alice@x.com", "Hi", "Hello") # ❌ TypeError
# / makes args before it positional-ONLY (Python 3.8+)
def distance(x, y, /):
return (x**2 + y**2) ** 0.5
distance(3, 4) # ✅
distance(x=3, y=4) # ❌ TypeError
Python resolves names in this order: Local → Enclosing → Global → Built-in. Similar to JS's scope chain.
let x = "global";
function outer() {
let x = "outer";
function inner() {
// Closure — sees outer x
console.log(x); // "outer"
}
inner();
}
// Modify outer from inner
function counter() {
let count = 0;
return () => {
count++; // closure captures count
return count;
};
}
x = "global"
def outer():
x = "outer"
def inner():
# Closure — sees outer x
print(x) # "outer"
inner()
# Modify outer from inner — needs 'nonlocal'
def counter():
count = 0
def increment():
nonlocal count # ← required to reassign
count += 1
return count
return increment
Use nonlocal to reassign a variable from an enclosing (but not global) scope. Use global to reassign a module-level variable. Both are rarely needed — if you find yourself using them a lot, refactor to a class or pass the value explicitly.
Python's import system is like JS require() / import — but the mental model is different. Every .py file is a module. Every directory with an __init__.py is a package.
// Named export
export function add(a, b) { return a + b; }
export const PI = 3.14;
// Default export
export default class Foo {}
// Import
import { add, PI } from './math.js';
import Foo from './foo.js';
import * as math from './math.js';
# math_utils.py — everything is exported by default
def add(a, b):
return a + b
PI = 3.14
# Importing
import math_utils # access as math_utils.add()
from math_utils import add # like named import
from math_utils import * # import everything (avoid this)
import math_utils as mu # alias
# There's no "default export" — just import by name
import os # operating system interface
import sys # system-specific params
import json # JSON encode/decode
import math # math functions
import re # regular expressions
import datetime # dates and times
from pathlib import Path # modern file path handling
from collections import defaultdict, Counter
This is Python's entry-point guard — equivalent to checking if a file is the "main" file in Node.js. Code inside this block only runs when the file is executed directly, not when it's imported.
def main():
print("Running!")
if __name__ == "__main__":
main()
1. What does *args capture in a function call?
2. What is the mutable default argument trap? Best fix?
3. To reassign a variable from an enclosing function scope, you use:
4. What does if __name__ == "__main__": guard against?
Questions answered correctly.