Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,13 @@ The **negative index** starts counting down from the end instead, starting at `-
print("invalid format")
```

- **Swapping variables:** unpack two values into each other's variables in one line, instead of using a temporary variable to hold one during the swap.

```python-ref
a, b = "ball python", "boa" # a="ball python" b="boa"
a, b = b, a # swaps directly — no temporary variable needed — a="boa" b="ball python"
```

### Tuple operations { data-card-link="skip" }

#### Inspect
Expand Down
253 changes: 165 additions & 88 deletions docs/errors.md

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions docs/foundations.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,31 @@ print(4.5) # when printing a number, you do not need quotes

Most sections on this site end with a collapsed block like the one below — open it, click **Run**, and try editing the code and running it again.

### Escape sequences

`\n` and `\t` are **escape sequences** — `\n` inserts a line break, `\t` a tab — so a single `print()` call can space out multi-line or columned output.

```python
print(f"species: burmese\nlength: 10 ft\n")
print("species\t\tlength_ft")
print("ball python\t4.5")
```

`\"`, `\'`, and `\\` escape a character that would otherwise end the string early or be read as another backslash — a quote matching the one the string started with, or a literal backslash itself.

```python
print("she said \"hello\"") # a double quote inside a double-quoted string
print('it\'s a python') # a single quote inside a single-quoted string
print("C:\\snakes") # a literal backslash
```

A row of repeated characters makes a quick visual separator between sections of console output, useful for breaking up a long script's output into readable chunks.

```python
print("survey results")
print("=" * 40)
```

### Going further { data-card-link="skip" }

??? run "Run a print() example"
Expand All @@ -152,6 +177,20 @@ Most sections on this site end with a collapsed block like the one below — ope
```python
print("hello, field guide")
print(4.5)


print(f"species: burmese\nlength: 10 ft\n")
print("species\t\tlength_ft")
print("ball python\t4.5")


print("she said \"hello\"")
print('it\'s a python')
print("C:\\snakes")


print("survey results")
print("=" * 40)
```

</div>
Expand Down
240 changes: 239 additions & 1 deletion docs/functions.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
description: >-
Python functions explained with runnable examples: defining, calling, arguments,
*args/**kwargs, scope, and recursion.
*args/**kwargs, scope, recursion, and decorators.
---

# :material-function-variant:{ .lg .middle } Functions
Expand Down Expand Up @@ -111,6 +111,54 @@ def describe(species, length_ft=5, venomous=False):
describe(species="ball", venomous=True) # length_ft still uses its default
```

### Type hints

A **type hint** annotates a parameter or return value with the type it's expected to be — `species: str`, `length_ft: float`, `-> bool` — without Python enforcing it at runtime; it's documentation an editor or a separate type checker (like `mypy`) can check for you.

```python-ref
def is_unusually_long(species: str, length_ft: float) -> bool:
return length_ft > 5
```

A wrong type still runs — Python doesn't stop you from calling `is_unusually_long("ball python", "4.5")` with a string instead of a `float` — the hint only helps a tool catch the mismatch before you do, and helps a reader (or their editor) see what's expected without reading the function body.

### Keep functions focused

A function should do one thing. If you find yourself describing it with "and" — "loads the species *and* saves it *and* prints a summary" — it's probably three functions.

```python-ref
def load_and_describe(species): # doing too much
...

def load_species(species): # one job each
...

def describe(species):
...
```

Repeating the same few lines in multiple places is a sign to pull them into their own function instead — commonly called **DRY** ("don't repeat yourself"). It also means a fix only has to happen in one place, instead of every place the lines were copied to.

??? tip "Guard clauses: return early instead of nesting"
Handle the exception case first and return, rather than wrapping the rest of the function in an `else`. It keeps the normal path at the lowest indentation level, instead of nested one level deeper for every added check.

```python-ref
def describe(length_ft):
if length_ft > 0:
return f"{length_ft} ft"
else:
return "unknown length"
```

```python-ref
def describe(length_ft):
if length_ft <= 0:
return "unknown length"
return f"{length_ft} ft"
```

Both versions do the same thing — the second reads top to bottom without having to track which `if` branch you're inside.

### Going further { data-card-link="skip" }

??? tip "pass placeholder"
Expand Down Expand Up @@ -199,6 +247,12 @@ describe(species="ball", venomous=True) # length_ft still uses its default

print(describe("ball", venomous=True))
print(describe(species="burmese", length_ft=12))


def is_unusually_long(species: str, length_ft: float) -> bool:
return length_ft > 5

print(is_unusually_long("ball python", 6))
```

</div>
Expand Down Expand Up @@ -404,3 +458,187 @@ Every recursive function needs two parts:

</div>

<div class="pfg-section" markdown="block">

## Decorators

**`@decorator`** lets you add behavior to a function without editing the function's own code — write the behavior once, then apply it to as many functions as you want. It's written as `@decorator_name`, placed directly above a `def`, and takes one function in, returning a function out[^callable].

### Wrapping the call

A decorator can run its own code around a function call by returning a different function instead of the original — a **wrapper** that does something, calls the original, then returns. This is the shape behind most decorators you'll actually use — logging, timing, or checking permissions before letting a call through.

```python
def decorator_name(func): # func is the function being decorated (here it's "describe()")
def wrapper(): # desfines a new function that runs in place of func from now on
print("looking up a snake...")
func() # calls the original, still reachable through func
print("found it")
return wrapper # decorator returns the new function name

@decorator_name # decorator_name can be any name you pick
def describe(): # here is your regular function you are decorating
print("a python")

describe() # every function call now prints "looking up a snake...", "a python", then "found it"
```

`@decorator_name` reassigns `describe` to `wrapper.` Calling `describe()` now actually runs `wrapper()`, which calls the original through `func`. `wrapper` and `decorator_name` are just names, not special syntax — any valid identifier works for either one.

### Returning the original function

Not every decorator needs a wrapper — the only actual requirement is returning *some* function. `catalog` below doesn't define a new one at all, it just hands back `func` itself, unchanged, so its surrounding prints only run once, the moment `describe` is defined — never again on any later call to `describe()`.

```python
def catalog(func):
print("looking up a snake...")
func()
print("found it")
return func # func, not func() — a reference to the function, not a call to it

@catalog
def describe():
print("a python")

describe() # "a python" only — the surrounding prints already ran once, at decoration

@catalog
def count():
return 5

print(count()) # 5 — return values pass through untouched too
```

`@catalog` reassigns `describe` to whatever `catalog` returns. `return func()` would call it and hand back its result instead of the function itself — `None` here — breaking `describe` as something you can call again.

### Accepting arguments

`describe` above takes no arguments, so `wrapper` didn't need to accept any either. Most functions do take arguments — `describe` normally takes a `species`, for instance — and `wrapper` has to accept whatever the decorated function needs.

`*args` and `**kwargs` let `wrapper` accept anything and print exactly what came in — useful for seeing what a function was actually called with, whatever its shape:

```python
def decorator(func):
def wrapper(*args, **kwargs): # accepts any parameters, instead of a fixed signature
print(f"called with {args}") # shows exactly what was passed in
return func(*args, **kwargs) # then forwards it all to func
return wrapper

@decorator
def describe(species):
return f"a {species} python"

print(describe("ball")) # prints "called with ('ball',)", then "a ball python"

@decorator
def total_length(*lengths):
return sum(lengths)

print(total_length(5, 12, 8)) # prints "called with (5, 12, 8)", then 25 — same decorator, different signature
```

### Advanced uses

??? tip "Decorators with arguments"
A decorator that needs its own settings takes those arguments one level out — a function that *returns* a decorator, instead of being one directly. This is how a decorator like Flask's `@app.route("/users")` gets its own argument (the URL path), separate from whatever function it ends up decorating.

```python
def tag(label): # called first, with the decorator's own argument
def decorator(func): # this is the actual decorator tag(label) builds
def wrapper(*args, **kwargs):
print(f"[{label}]")
return func(*args, **kwargs)
return wrapper
return decorator

@tag("sighting")
def describe(species):
return f"a {species} python"

print(describe("ball"))
```

??? tip "Applying a decorator manually"
`@decorator_name` is shorthand for calling the decorator directly and reassigning the function's name yourself — the two lines below have the exact same effect as `@decorator` above a `def describe():`.

```python
def decorator(func):
def wrapper():
print("looking up a snake...")
func()
print("found it")
return wrapper

def describe():
print("a python")

describe = decorator(describe) # same effect as @decorator, written out by hand
describe() # prints "looking up a snake...", "a python", then "found it"
```

Writing it out by hand is useful when a function shouldn't always be decorated — `@` applies unconditionally, every time the function is defined, while the manual form can sit behind a condition:

```python
def decorator(func):
def wrapper():
print("looking up a snake...")
func()
print("found it")
return wrapper

debug = True

def describe():
print("a python")

if debug:
describe = decorator(describe) # only decorated when debug is True

describe()
```

??? note "Stacking decorators"
Multiple decorators on the same function apply bottom-up — the one closest to `def` wraps first, and each one after it wraps the result of the one before. Stacking is common whenever a function needs more than one independent behavior — a web view that's both registered at a URL and requires the user to be logged in, for instance.

```python
def bold(func):
def wrapper(*args, **kwargs):
return f"**{func(*args, **kwargs)}**" # wraps whatever it's given in **
return wrapper

def shout(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper() # uppercases whatever it's given
return wrapper

@bold
@shout
def describe(species):
return f"a {species} python"

print(describe("ball")) # shout wraps describe first, then bold wraps shout's result
```

??? tip "Preserving identity"
Decorating a function replaces its identity with the wrapper's — `describe.__name__` becomes `"wrapper"`, not `"describe"`, since Python only sees the function `decorator` returned. `functools.wraps` copies the original function's name, docstring, and other metadata onto the wrapper so introspection tools still see the right name. This matters beyond cosmetics — some frameworks (Flask included) use a decorated function's `__name__` internally, so skipping `@wraps` can break things that have nothing to do with printing a name.

```python
from functools import wraps

def decorator(func):
@wraps(func) # copies func's __name__, __doc__, etc. onto wrapper
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper

@decorator
def describe(species):
return f"a {species} python"

print(describe.__name__) # "describe" — without @wraps(func), this would be "wrapper" instead
```

[^callable]: Technically a decorator just needs to return something *callable* — every decorator on this page returns a function specifically, but not all decorators do. [Classes](oop.md#method-decorators)' built-in `@property`, `@staticmethod`, and `@classmethod` return other kinds of callable object instead.

</div>

Loading
Loading