diff --git a/STRUCTURE.md b/STRUCTURE.md index 970931c..9511cfb 100644 --- a/STRUCTURE.md +++ b/STRUCTURE.md @@ -131,6 +131,13 @@ staying inline.** - **Keep subheading text short** — 1-2 words or a method/keyword name — since `toc.integrate` mirrors it verbatim into the sidebar. The fuller "why open this" context belongs in the first sentence under the heading, not the heading itself. +- **No backticks in heading text** — even for a heading that's naming a piece of syntax (e.g. + `#### *args tuple`, not `` #### `*args` tuple ``). Backticks are fine, and expected, in the + body prose under the heading. **Exception:** an identifier containing a double underscore + (`__init__`, `__repr__`, any dunder) needs its backticks kept — Markdown reads bare `__` as + bold/emphasis markup, not literal underscores, so `### Overriding __init__()` renders (and + slugifies) as "Overriding **init**()" with the underscores silently eaten. Confirm any heading + change like this against the real built HTML, not just the source Markdown. - **`####` is reserved** for `index.md`'s homepage category boxes and genuinely deep library-page content (e.g. `libraries/pillow.md`'s per-method sections) — most content pages should never need to go past `###`. @@ -177,14 +184,13 @@ this reason. `tests/test_homepage_keyword_links_cover_all_headings` knows the omission is deliberate rather than flagging it as a gap. Any other heading just needs *a* link to its anchor — the test doesn't check the link's text, so renaming an entry (or the heading) is a manual concern. -- **Order by heading level first, importance second — not top-to-bottom page order.** All `##` - entries come first, then all `###` entries, then any `####`/no-heading entries last; within - each of those tiers, sort most-to-least important rather than by page position. The two - orderings often coincide (pages are usually written in a sensible teaching order already), but - don't assume it — within the `##` tier, lead with the concept the card's own one-line - description is about; within the `###`/`####` tier, lead with the most commonly-needed related - syntax and put edge cases, advanced variants, or purely organizational headings (e.g. a page's - own "Common patterns" container heading) last in their tier. +- **Bold `##` entries stay in page order; their plain children are alphabetized.** Each `##` + heading gets its own bold entry (e.g. `` [**`def`**](functions.md#defining-a-function) ``), + and those bold entries keep the page's own top-to-bottom heading order — don't reshuffle them. + The flat list of plain (non-bold) links under a bold entry — its `###`/`####` children, plus + any bare-syntax entries with no heading of their own — sorts alphabetically by link text + (case-insensitive), not by importance or page position. Symbols sort before letters (plain + ASCII order), so a line like `` [`+= -= *= /=`] `` lands ahead of `` [`abs`] ``. - **Verify with a real build, not by eye** — `mkdocs build` prints a `WARNING` for every anchor/link it can't resolve; treat a clean build as the actual pass/fail check for this list, since hand-checked slugs are easy to get subtly wrong (trailing punctuation, duplicate-heading diff --git a/docs/oop.md b/docs/classes.md similarity index 58% rename from docs/oop.md rename to docs/classes.md index 31c8f27..378ff39 100644 --- a/docs/oop.md +++ b/docs/classes.md @@ -4,33 +4,9 @@ description: >- attributes, methods, property/staticmethod/classmethod, and inheritance. --- -# :material-package-variant:{ .lg .middle } Classes & Object-oriented programming (OOP) - -**Object-oriented programming** groups related data and the functions that act on it into a single unit, instead of keeping them separate. A [dictionary](collections.md#dictionaries) can already hold a snake's data as key-value pairs — a **class** goes one step further, bundling that data together with the behavior (methods) that belongs to it. - -
- -```mermaid -classDiagram - class Snake { - +species - +length_ft - +__init__(species, length_ft) - +describe() - +__str__() - } - class Boa { - +region - +__init__(species, length_ft, region) - +describe() - +habitat() - } - Snake <|-- Boa -``` - -

FIG: example of a class

+# :material-package-variant:{ .lg .middle } Classes -
+A **class** bundles related data together with the behavior (methods) that acts on it, instead of keeping them separate. A [dictionary](collections.md#dictionaries) can already hold a snake's data as key-value pairs — a class goes one step further, pairing that data with the functions that work on it. Structuring code this way is called **object-oriented programming (OOP)**. | Concept | Example | What it is | |---------|---------|------------| @@ -42,10 +18,20 @@ classDiagram
-## Classes and objects +## Defining a class A class is a blueprint for creating objects — it defines what attributes and methods every object built from it will have. An object is one specific instance built from that blueprint, with its own copy of the attributes. +The class definition line contains **`class`**, a **class name** (capitalized in PascalCase, unlike variables' `snake_case`), and a **colon**. Under it is an indented **body** — usually starting with `__init__`, the method that sets up a new object's starting attributes. + +```python-ref +class ClassName: + def __init__(self, parameter): + self.attribute = parameter +``` + +Create an object by calling the class like a function: `ClassName(argument)`. + ```python-ref class Snake: def __init__(self, species, length_ft): @@ -58,9 +44,18 @@ print(ball.species) print(ball.length_ft) ``` +What `ball = Snake("ball", 5)` does: + +0. Creates a new, empty object. +1. Calls `__init__` automatically, passing that object in as `self`, plus the arguments given — `"ball"` and `5`, matching `species` and `length_ft`. +2. `self.species = species` and `self.length_ft = length_ft` store those as **attributes** — data belonging to this one object, not to the `Snake` class as a whole. +3. Stores the finished object in `ball`. + +`burmese = Snake("burmese", 16)` builds a separate object the same way — `burmese.species` and `ball.species` don't share data, same as two function calls (previous page) don't share local variables. + ### The `__init__()` method -Runs automatically every time a new object is created. It's where you set up the object's starting attributes. Python calls this a **constructor**. +Runs automatically every time a new object is created — step 1 above. It's where an object's starting attributes get set up. Python calls this a **constructor**. You never call `__init__()` directly — `Snake("ball", 5)` is what triggers Python to call it. ```python-ref ball = Snake("ball", 5) # __init__ runs automatically, setting ball.species and ball.length_ft @@ -91,22 +86,80 @@ ball = Snake("ball", 5) # __init__ runs automatically, setting ball.species a self.tags = tags if tags is not None else [] # a new list every time ``` -### The `self` parameter +### The self parameter -Refers to the specific object a method was called on. `self` is the first parameter of every method in a class — it's how `ball.species` and `burmese.species` hold different values while sharing the same class. Python passes it in automatically; you never supply it yourself when calling a method (`ball.describe()`, not `ball.describe(ball)`). +Refers to the specific object a method was called on. One `Snake` class, but many `Snake` objects (`ball`, `burmese`, ...) sharing its method code — `self` is how a method written once still knows which object to act on. + +`self` is always a method's first parameter, filled in automatically by Python — you never supply it yourself (`ball.describe()`, not `ball.describe(ball)`). Writing `ball.describe()` is what passes `ball` in as `self`. ```python-ref self.species # inside a method, refers to *this* object's own species — "ball" for ball, "burmese" for burmese ``` +Same method, different object, different `self`: + +```python-ref +ball.describe() # self is ball → "a 5 ft ball python" +burmese.describe() # self is burmese → "a 16 ft burmese python" +``` + ### Object methods -A method is just a function defined inside a class. Since it always receives `self`, it can read (or change) that specific object's own attributes. +A method is a function defined inside a class — parameters, `return`, and defaults all work the same as on the [Functions](functions.md) page. The one addition is `self`, which lets it read or change that specific object's own attributes. ```python-ref ball.describe() # "a 5 ft ball python" ``` +### Instance attributes + +An instance attribute is set with `self.x = value`, usually inside `__init__`. This is the default way a class stores data — each object gets its own independent copy, separate from every other object's. + +```python-ref +class Snake: + def __init__(self, species, length_ft): + self.species = species # instance attribute + self.length_ft = length_ft + +ball = Snake("ball", 5) +burmese = Snake("burmese", 16) + +print(ball.species) # "ball" +print(burmese.species) # "burmese" — a separate copy, not shared +``` + +For a value every object should share instead of holding its own copy, see [class attributes](#class-attributes) below. + +### Class attributes + +A class attribute is set directly in the class body, outside `__init__` — shared by every object built from that class, unlike an [instance attribute](#instance-attributes), which is a separate copy per object. Assigning to `object.attribute` always creates (or updates) an instance attribute, even if a class attribute of the same name exists — it doesn't change the shared value, just shadows it for that one object. + +```python-ref +class Snake: + kingdom = "Animalia" # class attribute — shared by every Snake object + + def __init__(self, species, length_ft): + self.species = species # instance attribute — its own copy per object + self.length_ft = length_ft + +ball = Snake("ball", 5) +burmese = Snake("burmese", 16) + +print(ball.kingdom) # "Animalia" +print(burmese.kingdom) # "Animalia" — same value, shared + +ball.kingdom = "Reptilia" # creates an instance attribute — doesn't touch the class attribute +print(ball.kingdom) # "Reptilia" — this object's own copy now +print(burmese.kingdom) # "Animalia" — unaffected +``` + +| | Instance attribute | Class attribute | +|---|---|---| +| Set with | `self.x = value`, usually in `__init__` | `x = value` directly in the class body | +| Copies | One per object | One, shared by every object | +| Changing it on one object | Only that object sees the change | Reassigning through the class changes it for every object that hasn't shadowed it | +| Use it for | Data that's different for each object — `species`, `length_ft` | A value every object of the class shares — a constant, a shared default, a running count | + ### Going further { data-card-link="skip" } ??? tip "The `__str__()` method" @@ -342,7 +395,7 @@ class Boa(Snake): self.region = region ``` -### Using `super()` +### Using super() Calls the parent's version of a method without naming the parent class directly. The usual, cleaner way to do what the previous example did by hand. @@ -368,6 +421,28 @@ snake.describe() # "a 5 ft ball python" — Snake's own version boa.describe() # "a heavy-bodied constrictor" — Boa's version replaces it ``` +### Multiple inheritance { data-advanced="true" } + +A class can list more than one parent, comma-separated — it inherits the combined attributes and methods of all of them. When two parents define the same method, Python searches left to right through the parents listed and uses the first match — this search order is called the **MRO** (method resolution order). + +```python-ref +class Venomous: + def warning(self): + return "handle with extreme caution" + +class Constrictor: + def warning(self): + return "handle with care, can constrict" + +class Cobra(Venomous, Constrictor): + pass + +cobra = Cobra() +print(cobra.warning()) # "handle with extreme caution" — Venomous is listed first +``` + +`Cobra.__mro__` shows the actual search order Python used, in case more than two parents makes it unclear. + ### Going further { data-card-link="skip" } ??? run "Run an inheritance example" @@ -475,7 +550,7 @@ print(len(["ball", "burmese", "boa"])) print(len({"species": "ball", "length_ft": 5})) ``` -### Same method name, unrelated classes +### Duplicate method names Classes don't need to be related by inheritance to share a method name. As long as each one defines its own `.move()`, calling it works the same way no matter which object it's called on. @@ -543,3 +618,176 @@ for s in (snake, boa): print(s.describe())
+
+ +## Encapsulation { data-advanced="true" } + +**Encapsulation** restricts direct access to an object's data, so it can only be read or changed through the class's own methods. Python doesn't enforce this the way some other languages do — it's a naming convention the caller is trusted to respect, not a hard restriction. + +### Single underscore + +A leading underscore (`_species`) signals "internal — not part of the class's public interface." Python doesn't actually stop outside code from reading or changing it; it's a convention, not a lock. + +```python-ref +class Snake: + def __init__(self, species, length_ft): + self._species = species # leading underscore — treat as internal + +ball = Snake("ball", 5) +ball._species # "ball" — still accessible, just a signal not to +``` + +### Double underscore + +A leading double underscore (`__species`) triggers **name mangling** — Python renames the attribute internally to `_ClassName__species`, making it awkward (though still not impossible) to reach from outside the class. + +```python-ref +class Snake: + def __init__(self, species, length_ft): + self.__species = species # name-mangled + +ball = Snake("ball", 5) +ball.__species # AttributeError — not found under this name +ball._Snake__species # "ball" — the actual mangled name +``` + +### Controlled access with @property + +Pair an underscore-prefixed attribute with [`@property`](#property) to actually enforce something — like validation — instead of only signaling intent. + +```python-ref +class Snake: + def __init__(self, species, length_ft): + self._length_ft = length_ft + + @property + def length_ft(self): + return self._length_ft + + @length_ft.setter + def length_ft(self, value): + if value <= 0: + raise ValueError("length_ft must be positive") + self._length_ft = value + +ball = Snake("ball", 5) +ball.length_ft = -1 # ValueError — blocked by the setter +``` + +
+ +
+ +## Operator overloading { data-advanced="true" } + +Defining a dunder method lets a built-in operator (`==`, `<`, `+`, ...) work on your own objects — the same mechanism as [`__str__()`](#defining-a-class) and [`__repr__()`](#defining-a-class), just for operators instead of printing. + +```python-ref +ball = Snake("ball", 5) +ball == Snake("ball", 5) # False — without __eq__, Python compares by identity, not by data +``` + +### Comparing with `__eq__` and `__lt__` + +`__eq__` defines what `==` does; `__lt__` defines what `<` does. Without them, `==` falls back to comparing identity (is this the exact same object?) rather than the data inside. + +```python-ref +class Snake: + def __init__(self, species, length_ft): + self.species = species + self.length_ft = length_ft + + def __eq__(self, other): + return self.length_ft == other.length_ft + + def __lt__(self, other): + return self.length_ft < other.length_ft + +ball = Snake("ball", 5) +burmese = Snake("burmese", 16) + +print(ball == Snake("ball", 5)) # True — same length_ft +print(ball < burmese) # True — 5 < 16 +``` + +### Arithmetic with `__add__` + +`__add__` defines what `+` does between two objects — whatever combining them should mean for this class. + +```python-ref +class Snake: + def __init__(self, species, length_ft): + self.species = species + self.length_ft = length_ft + + def __add__(self, other): + return self.length_ft + other.length_ft + +ball = Snake("ball", 5) +burmese = Snake("burmese", 16) + +print(ball + burmese) # 21 — combined length +``` + +
+ +
+ +## Dataclasses { data-advanced="true" } + +`@dataclass` generates `__init__()` and `__repr__()` automatically from a list of typed attributes, instead of writing them by hand. + +```python-ref +from dataclasses import dataclass + +@dataclass +class Snake: + species: str + length_ft: float + +ball = Snake("ball", 5) +print(ball) # Snake(species='ball', length_ft=5) — __repr__ generated automatically +``` + +Equivalent to writing the same class by hand: + +```python-ref +class Snake: + def __init__(self, species, length_ft): + self.species = species + self.length_ft = length_ft + + def __repr__(self): + return f"Snake(species={self.species!r}, length_ft={self.length_ft!r})" +``` + +Use it for a class that's mostly just holding data, with little or no custom behavior; skip it once a class needs real logic beyond storing and reporting its attributes. + +
+ +
+ +## Abstract base classes { data-advanced="true" } + +An **abstract base class** defines methods that every subclass must implement, using `abc.ABC` and `@abstractmethod`. Trying to create an object from a class that hasn't implemented all of them raises a `TypeError` immediately, instead of failing later when the missing method actually gets called. + +```python-ref +from abc import ABC, abstractmethod + +class Snake(ABC): + @abstractmethod + def move(self): + ... + +class Boa(Snake): + def move(self): + return "slither" + +boa = Boa() # works — Boa implements move() +snake = Snake() # TypeError — can't instantiate abstract class with abstract method 'move' +``` + +Use it when a base class should only ever be a template — never instantiated directly — and every subclass must supply certain methods; skip it for ordinary inheritance where the base class already works fine on its own, as with `Snake` and `Boa` [earlier on this page](#inheritance). + +
+ diff --git a/docs/collections.md b/docs/collections.md index 4859bb3..2ce9ddf 100644 --- a/docs/collections.md +++ b/docs/collections.md @@ -77,7 +77,7 @@ class diagram panel - **Index with `list[index]`** to return the item at that index (position number) of the list. - To **update** the item at that index, set it equal to something else **`list[index] = new_item`**. This works because a list is **mutable** — updating an item changes it in place instead of building a new one, the same way [an object's attributes](oop.md#classes-and-objects) can be changed after it's created. + To **update** the item at that index, set it equal to something else **`list[index] = new_item`**. This works because a list is **mutable** — updating an item changes it in place instead of building a new one, the same way [an object's attributes](classes.md#defining-a-class) can be changed after it's created. *Run the below example, and change the indexes to see how they work:* @@ -331,6 +331,8 @@ class diagram panel [s.title() for s in species] # ["Burmese", "Rock", "Ball", "Blood"] ``` + Swapping the brackets for parentheses turns this into a [generator expression](functions.md#generator-expressions) instead — same syntax, but it produces items one at a time rather than building the whole list up front. Use a list comprehension when the result needs indexing, `len()`, or looping over more than once; use a generator expression when it's only read once, or the full result would be too large to hold in memory as a list. + ### Going further { data-card-link="skip" } ??? warning "In-place list methods return None" diff --git a/docs/functions.md b/docs/functions.md index 8eef957..ff9dc17 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -6,126 +6,286 @@ description: >- # :material-function-variant:{ .lg .middle } Functions -A **function** packages a block of code under a name, so it can be run again — with different inputs — instead of copying and pasting the same lines every time you need them. Python already has some built in (`print()`, `len()`), but `def` lets you write your own. +A **function** packages a block of code under a name, so it can be run again — with different inputs — instead of copying and pasting the same lines every time you need them. -| Concept | Example | What it is | -|---------|---------|------------| -| Parameter | `def describe(species):` | A name a function expects to receive a value for, listed in its definition | -| Argument | `describe("ball")` | The actual value passed in when the function is called | -| Return value | `return f"a {species} python"` | The value a function sends back to whatever called it | -| Default value | `def describe(species="ball"):` | A fallback used when the caller doesn't supply that argument | +Python already has some built in (`print()`, `len()`, `input()`), but `def` lets you write your own. + +```python-ref +def describe(species): # function definition: "describe" is function name, "species" is a parameter + return f"a {species} python" # indented block of code that is run inside of function + +describe("ball") # function call: "ball" is an argument — the value passed in for species +message = describe("ball") # "a ball python" is the return value, so now "message" becomes equal to it +``` + +??? run "Run a function example" + All the examples above, combined into one script: + + ```python + def describe(species): + print(f"a {species} python") + + describe("ball") + describe("burmese") + + + def describe(species, length_ft): + print(f"a {length_ft} ft {species} python") + + describe("ball", 5) + describe(5, "ball") + + + def describe(species): + """Return a short description of the given snake species.""" + return f"a {species} python" + + print(describe("ball")) + print(describe.__doc__) + + + def describe(species): + return f"a {species} python" + + message = describe("ball") + print(message) + + + def find_species(name): + if name == "ball": + return "found it" + # falls through here for anything else — implicitly returns None + + result = find_species("cobra") + print(result) + + + def check_length(length_ft): + if length_ft > 10: + return "long snake" + return "short snake" # only reached if the if above didn't return + + print(check_length(12)) + print(check_length(4)) + + + def describe(species, length_ft=5): + return f"a {length_ft} ft {species} python" + + print(describe("ball")) + print(describe("burmese", 12)) + + + def describe(species, length_ft=5, venomous=False): + return f"{species}, {length_ft} ft, venomous: {venomous}" + + print(describe("ball", venomous=True)) + print(describe(species="burmese", length_ft=12)) + + + def describe(species: str, length_ft: float): + return f"a {length_ft} ft {species} python" + + print(describe("ball python", "4.5")) + + + def is_unusually_long(species: str, length_ft: float) -> bool: + return length_ft > 5 + + print(is_unusually_long("ball python", 6)) + ```
## Defining a function -`def` names a function and lists the parameters it expects; the indented block underneath is what runs each time it's called. +A function is first defined. After it's defined, you can [call the function](#calling-a-function) whenever you need to run it. + +The function definition line contains **`def`**, a **function name** (follows the same [naming rules as variables](foundations.md#naming-variables)), **parentheses** holding zero or more **parameters**, and a **colon**. + +Under it is an indented **body**: the block of code that runs when the function is [called](#calling-a-function). ```python-ref -def describe(species): - print(f"a {species} python") +def function_name(optional_parameter, optional_parameter): + [indented block of code, run whenever the function is called] -describe("ball") -describe("burmese") ``` -??? tip "Indenting a block" - Select a line (or several), then indent or unindent it in one keystroke instead of retyping spaces. These are the defaults in VS Code, PyCharm, Thonny, and IDLE. Selecting multiple lines first — click and drag, or hold ++shift++ while using the arrow keys — indents or unindents all of them together, which matters here since every line inside a function body needs the same indentation. +??? tip "Shortcut for indenting multiple lines" + Select several lines, then indent or unindent them in one keystroke. Selecting multiple lines first — click and drag, or hold ++shift++ while using the arrow keys — indents or unindents all of them together, which matters here since every line inside a function body needs the same indentation. | Action | Shortcut | |--------|------------------| | Indent selected lines | ++tab++ | | Unindent selected lines | ++shift+tab++ | -### Docstrings +### Parameters -A triple-quoted string as a function's first line documents what it does — most editors show it automatically when you use the function elsewhere. A **docstring** is the same triple-quoted-string trick covered on the [Foundations](foundations.md#multi-line-comments-with) page, but placed as the very first line inside a function specifically to document it. Unlike a regular comment, Python actually stores a docstring (as the function's `__doc__` attribute) rather than discarding it — which is how editors are able to show it in a tooltip when you call the function elsewhere, without you needing to go find the definition. +A **parameter** is the placeholder name listed in a function's own definition — as opposed to an **argument**, the actual value a caller passes in for it. -Short, single-line docstrings are common for simple functions: +A function can list zero, one, or multiple parameters separated by commas. Arguments are matched to parameters by position — the first argument fills the first parameter, the second fills the second, and so on. + +Calling with too few or too many arguments raises a `TypeError` — Python doesn't know which value goes where. Arguments can also be matched by name instead of position — see [keyword arguments](#keyword-arguments) under calling a function. ```python-ref -def describe(species): - """Return a short description of the given snake species.""" - return f"a {species} python" +def describe(species, length_ft): + print(f"a {length_ft} ft {species} python") + +describe("ball", 5) # a 5 ft ball python +describe(5, "ball") # a ball ft 5 python — wrong order, but still runs ``` -For a function where you want to document its parameters or return values, you can spell them out using this standard format. You list all parameters/arguments and their name, type, and description, the return type and description, and the one-line summary: +#### Default values -```python-ref -def is_too_long(species, length_ft): - """ - Check whether a snake is unusually long for its species. +A parameter can fall back to a default value if the call doesn't specify one. Parameters with a default must come **after** all of the parameters without one. - Args: - species (str): the snake's species name. - length_ft (float): the snake's measured length, in feet. +```python-ref +def describe(species, length_ft=5): # default length_ft is 5, if not given then called. + return f"a {length_ft} ft {species} python" - Returns: - bool: True if length_ft is unusually long for species. - """ - return length_ft > 5 +describe("burmese", 12) # length_ft is 12 +describe("ball") # second parameter is not given, so length_ft is the default 5 ``` -### Return values +??? warning "Mutable default argument" + A default value is only ever created **once**, when the function is defined — not fresh on every call. For a list or dict default, that means every call sharing that default is silently reading and writing the *same* object, so it keeps growing across calls instead of starting empty each time. + + ```python-ref + def add_sighting(species, log=[]): # log=[] is created once, not per call + log.append(species) + return log + + add_sighting("ball") # ["ball"] + add_sighting("burmese") # ["ball", "burmese"] — the same list, not a fresh one + ``` + + Default to `None` instead, and create the list inside the function body: + + ```python-ref + def add_sighting(species, log=None): + if log is None: + log = [] + log.append(species) + return log + ``` -`return` sends a value back to the caller, instead of just printing it. `return` also exits the function immediately, skipping any code written after it. +#### *args tuple + +`*args` collects any number of positional arguments into a single [tuple](collections.md#tuples), so a function can accept as many as the caller passes instead of a fixed list of parameters. `*args` is the conventional name, but any name after `*` works. ```python-ref -def describe(species): - return f"a {species} python" +def total_length(*args): + return sum(args) # args is (5, 12, 8) inside the function -message = describe("ball") # "a ball python" — stored, not printed +total_length(5, 12, 8) # 25 ``` -If a function runs to the end without hitting a `return` statement, it returns `None` automatically. This is what you get back if a lookup silently "doesn't find" anything. +#### **kwargs dict + +`**kwargs` collects any number of keyword arguments into a single [dict](collections.md#dictionaries), so a function can accept as many `name=value` pairs as the caller passes instead of a fixed list of parameters. `**kwargs` is the conventional name, but any name after `**` works. ```python-ref -def find_species(name): - if name == "ball": - return "found it" - # falls through here for anything else — implicitly returns None +def describe(**details): + return details # {"species": "ball", "length_ft": 5} -result = find_species("cobra") # None — the function fell through without a return +describe(species="ball", length_ft=5) ``` -### Default parameter values +#### Type hints { data-advanced="true" } -A parameter can fall back to a default value if the caller doesn't supply one. Parameters with a default must come after every parameter without one — Python reads arguments left to right, so a required parameter can't follow an optional one. +A type hint on a parameter like `species: str` annotates the type of value it's expected to receive. Python doesn't enforce it, but it can be helpful for you to keep track of it and a separate type checker (like `mypy`) can check for you. ```python-ref -def describe(species, length_ft=5): +def describe(species: str, length_ft: float): return f"a {length_ft} ft {species} python" +``` + +#### Combining categories { data-advanced="true" , data-card-link="skip" } -describe("ball") # "a 5 ft ball python" — uses the default -describe("burmese", 12) # "a 12 ft burmese python" — overrides it +A single signature can mix kinds of parameters, but must be in this order: + +1. positional parameters +2. `*args` +3. keyword-only parameters +4. `**kwargs` + +`venomous` sits after `*lengths`, which makes it keyword-only automatically — anything named after `*args` can only be passed by name, even without a separate bare `*`. + +```python-ref +def describe(species, *lengths, venomous=False, **details): + return species, lengths, venomous, details + +describe("ball", 5, 6, venomous=True, habitat="captive") +# species = "ball", lengths = (5, 6), venomous = True, details = {"habitat": "captive"} ``` -### Keyword arguments +#### Positional-only { data-advanced="true" } -Passing `name=value` lets you specify arguments out of order, or skip earlier defaults. Arguments passed by position (like `describe("ball")`) must still come first; keyword arguments can follow in any order, and are matched by name instead of position. +A `/` in the parameter list marks every parameter before it **positional-only** — it can only be passed by position, never by name. Most parameters don't need this restriction. It mainly shows up in library code, where locking a parameter to positional-only lets the author rename it later without breaking callers who passed it by keyword. ```python-ref -def describe(species, length_ft=5, venomous=False): - return f"{species}, {length_ft} ft, venomous: {venomous}" +def describe(species, /, length_ft): + return f"{species}, {length_ft} ft" -describe(species="ball", venomous=True) # length_ft still uses its default +describe("ball", 5) # by position — works +describe(species="ball", length_ft=5) # TypeError — species is positional-only ``` -### Type hints +#### Keyword-only { data-advanced="true" } -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. +A `*` in the parameter list marks every parameter after it **keyword-only** — it can only be passed by name, never by position. Keyword-only parameters suit options that would be unclear as a bare positional value — `venomous=True` reads clearly at the call site, `True` alone wouldn't. ```python-ref -def is_unusually_long(species: str, length_ft: float) -> bool: - return length_ft > 5 +def describe(species, *, venomous): + return f"{species}, venomous: {venomous}" + +describe("ball", venomous=True) # by name — works +describe("ball", True) # TypeError — venomous is keyword-only +``` + +### Return values + +`return` sends a value back to whatever called the function, instead of just printing it. `return` also exits the function immediately, skipping any code written after it. + +`message = describe("ball")`: `describe` runs with `species` set to `"ball"`, builds the string, then `return` hands it back to the `=` that called it — `message` now holds `"a ball python"`, nothing gets printed. + +That's the difference from `print()`: `print()` shows a value and discards it; `return` hands the value back to be stored, passed along, or used in another expression. + +```python-ref +def describe(species): + return f"a {species} python" + +message = describe("ball") # "a ball python" — stored, not printed +``` + +If a function runs to the end without hitting a `return` statement, it returns `None` automatically. This is what you get back if a lookup silently "doesn't find" anything. + +```python-ref +def find_species(name): + if name == "ball": + return "found it" + # falls through here for anything else — implicitly returns None + +result = find_species("cobra") # None — the function fell through without a return ``` -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. +#### Multiple values { data-card-link="skip" } + +`return` followed by several values separated by commas [packs](collections.md#packing-and-unpacking) them into a tuple as a single return value. The caller unpacks that tuple to use the values separately — see [multiple values](#multiple-values_1) under calling a function. -### Keep functions focused +```python-ref +def describe(species, length_ft): + return species, length_ft # packs ("ball", 5) into one tuple, once called + +species, length = describe("ball", 5) # name = "ball", length = 5 +``` + +### Keep functions focused { data-card-link="skip" } 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. +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. + ```python-ref def load_and_describe(species): # doing too much ... @@ -137,8 +297,6 @@ 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. @@ -159,171 +317,135 @@ Repeating the same few lines in multiple places is a sign to pull them into thei 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" - Temporarily fill an empty function body when you're not ready to write the inside code yet. Python doesn't allow an empty block after a colon. `pass` does nothing, but acts as a placeholder until you're ready to add code so that the empty block won't cause a syntax error in the meantime. Covered in more detail on the [Conditionals](conditionals.md#if-elif-else) page. +### pass placeholder - ```python-ref - def describe(species): - pass # placeholder — does nothing, but prevents a syntax error - ``` - -??? warning "Mutable default argument" - A default value is only ever created **once**, when the function is defined — not fresh on every call. For a list or dict default, that means every call sharing that default is silently reading and writing the *same* object, so it keeps growing across calls instead of starting empty each time. - - ```python-ref - def add_sighting(species, log=[]): # log=[] is created once, not per call - log.append(species) - return log - - add_sighting("ball") # ["ball"] - add_sighting("burmese") # ["ball", "burmese"] — the same list, not a fresh one - ``` - - Default to `None` instead, and create the list inside the function body: - - ```python-ref - def add_sighting(species, log=None): - if log is None: - log = [] - log.append(species) - return log - ``` - -??? run "Run a function example" - All the examples above, combined into one script: - - ```python - def describe(species): - print(f"a {species} python") - - describe("ball") - describe("burmese") - - - def describe(species): - """Return a short description of the given snake species.""" - return f"a {species} python" - - print(describe("ball")) - print(describe.__doc__) +`pass` temporarily fills an empty function block so it doesn't raise a syntax error while you're not ready to write the real code yet. +```python-ref +def describe(species): + pass # placeholder — does nothing, but prevents a syntax error +``` - def describe(species): - return f"a {species} python" - message = describe("ball") - print(message) +### Docstrings +A triple-quoted string as a function's first line documents what it does — most editors show it automatically when you use the function elsewhere. A **docstring** is the same triple-quoted-string trick covered on the [Foundations](foundations.md#multi-line-comments-with) page, but placed as the very first line inside a function specifically to document it. Unlike a regular comment, Python actually stores a docstring (as the function's `__doc__` attribute) rather than discarding it — which is how editors are able to show it in a tooltip when you call the function elsewhere, without you needing to go find the definition. - def find_species(name): - if name == "ball": - return "found it" - # falls through here for anything else — implicitly returns None +Short, single-line docstrings are common for simple functions: - result = find_species("cobra") - print(result) +```python-ref +def describe(species): + """Return a short description of the given snake species.""" + return f"a {species} python" +``` +For a function where you want to document its parameters or return values, you can spell them out using this conventional format. In addition to the summary, you also list all parameters/arguments and their name, type, and description, the return type and description: - def check_length(length_ft): - if length_ft > 10: - return "long snake" - return "short snake" # only reached if the if above didn't return +```python-ref +def is_too_long(species, length_ft): + """ + Check whether a snake is unusually long for its species. - print(check_length(12)) - print(check_length(4)) + Args: + species (str): the snake's species name. + length_ft (float): the snake's measured length, in feet. + Returns: + bool: True if length_ft is unusually long for species. + """ + return length_ft > 5 +``` - def describe(species, length_ft=5): - return f"a {length_ft} ft {species} python" +
- print(describe("ball")) - print(describe("burmese", 12)) +
+## Calling a function - def describe(species, length_ft=5, venomous=False): - return f"{species}, {length_ft} ft, venomous: {venomous}" +`describe("ball")` is the call — the name, followed by parentheses, is what runs the body. `"ball"` fills in `species` for that one run. Same body, run twice — only the value in `species` changes between calls. - print(describe("ball", venomous=True)) - print(describe(species="burmese", length_ft=12)) +```python-ref +describe("ball") # a ball python +describe("burmese") # a burmese python +``` +??? run "Run a calling a function example" + All the examples above, combined into one script: - def is_unusually_long(species: str, length_ft: float) -> bool: - return length_ft > 5 + ```python + def describe(species): + print(f"a {species} python") - print(is_unusually_long("ball python", 6)) + describe("ball") + describe("burmese") ``` -
+### Arguments -
+An **argument** is the actual value a caller passes in for a parameter — as opposed to a **parameter**, the placeholder name listed in a function's own definition. -## Flexible arguments +#### Required -`*args` and `**kwargs` let a function accept an unpredictable number of arguments, instead of a fixed list of parameters. +By default, a call needs an argument for every parameter that doesn't have one already, supplied in the same order the parameters were listed — unless passed [by keyword](#by-keyword) instead. Leaving one out, or supplying too many, raises a `TypeError`. ```python-ref -def total_length(*lengths): - return sum(lengths) +def describe(species, length_ft): + return f"{species}, {length_ft} ft" -print(total_length(5, 12, 8)) +describe("ball", 5) # both required arguments supplied, by position +describe("ball") # TypeError — missing required argument: 'length_ft' ``` -### `*args` +#### By keyword -Collects any number of positional arguments into a tuple. `*lengths` gathers however many positional arguments were passed into a single tuple named `lengths` — the function works the same whether it's called with one length or ten. `*args` is the conventional name, but any name after `*` works. +Passing `name=value` lets you specify arguments out of order, or skip earlier defaults. Arguments passed by position (like `describe("ball")`) must still come first; keyword arguments can follow in any order, and are matched by name instead of position. A function can also catch any number of these in one parameter — see the [`**kwargs` dict](#kwargs-dict) under defining a function. ```python-ref -def total_length(*lengths): - return sum(lengths) # lengths is (5, 12, 8) inside the function +def describe(species, length_ft=5, venomous=False): + return f"{species}, {length_ft} ft, venomous: {venomous}" -total_length(5, 12, 8) # 25 +describe(species="ball", venomous=True) # length_ft still uses its default ``` -### `**kwargs` +#### Unpacking -Collects any number of keyword arguments into a dict. `**details` gathers every `name=value` keyword argument into a dict named `details`, keyed by argument name. `**kwargs` is the conventional name, but like `*args`, any name after `**` works. +`*` and `**` also work in a function call, where they do the reverse of `*args`/`**kwargs`: instead of gathering separate arguments into one tuple or dict, they spread an existing list or dict back out into separate arguments. `*` unpacks a list or tuple into positional arguments; `**` unpacks a dict into keyword arguments. This is the call-site mirror of the [`*args` tuple](#args-tuple) and [`**kwargs` dict](#kwargs-dict) under defining a function — those gather a variable number of arguments into a tuple or dict at definition time; unpacking spreads a list, tuple, or dict back into individual arguments at the call site. ```python-ref -def describe(**details): - return details # {"species": "ball", "length_ft": 5} +values = ["ball", 5] +describe(*values) # same as describe("ball", 5) -describe(species="ball", length_ft=5) +details = {"species": "ball", "length_ft": 5} +describe(**details) # same as describe(species="ball", length_ft=5) ``` -### Going further { data-card-link="skip" } - -??? run "Run a flexible arguments example" - All the examples above, combined into one script: - - ```python - def total_length(*lengths): - return sum(lengths) +### Saving the return value - print(total_length(5, 12, 8)) +Assign the call to a variable to keep the value `return` sent back, instead of it being discarded. `message = describe("ball")` runs `describe` with `species` set to `"ball"`, and `return` hands the built string back to the `=` that called it — `message` now holds `"a ball python"`. That's the difference from `print()`: `print()` shows a value and discards it; `return` hands the value back to be stored, passed along, or used in another expression. +```python-ref +def describe(species): + return f"a {species} python" - def total_length(*lengths): - print(lengths) - return sum(lengths) +message = describe("ball") # "a ball python" — stored, not printed +``` - print(total_length(5, 12, 8)) - print(total_length(4.5)) +#### Multiple values { data-card-link="skip" } +A function that [returns multiple values packed into a tuple](#multiple-values) can have them unpacked straight into multiple variables in one line at the call site. `name, length = describe(...)` unpacks the returned tuple, matching each variable to the tuple's items by position — the same as [unpacking any other tuple](collections.md#packing-and-unpacking). The number of variables on the left has to match the number of values returned. - def describe(**details): - for key, value in details.items(): - print(key, value) +```python-ref +def describe(species, length_ft): + return species, length_ft - describe(species="ball", length_ft=5, venomous=False) - ``` +name, length = describe("ball", 5) # name = "ball", length = 5 +```
-## Scope { data-advanced="true" } +## Scope A variable created inside a function is **local** — it only exists while that function is running, and isn't visible outside it. @@ -346,8 +468,6 @@ def show_species(): print(species) # reads the global — no error ``` -### Going further { data-card-link="skip" } - ??? tip "Modifying a global variable" `global` tells Python that an assignment inside a function should change the global variable, not create a local one. Without `global`, `count += 1` here would raise an error — Python sees the assignment and treats `count` as local for the whole function, then finds no local `count` to add to. `global` is needed occasionally, but reaching for it often is usually a sign the code would read more clearly passing values in and returning them instead. @@ -468,6 +588,8 @@ Every recursive function needs two parts: 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. +`@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. + ```python def decorator_name(func): # func is the function being decorated (here it's "describe()") def wrapper(): # defines a new function that runs in place of func from now on @@ -483,12 +605,12 @@ def describe(): # here is your regular function you are decorating 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()`. +`@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. + ```python def catalog(func): print("looking up a snake...") @@ -509,8 +631,6 @@ def count(): 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. @@ -638,7 +758,120 @@ print(total_length(5, 12, 8)) # prints "called with (5, 12, 8)", then 25 — 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. +[^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](classes.md#method-decorators)' built-in `@property`, `@staticmethod`, and `@classmethod` return other kinds of callable object instead. + +
+ +
+ +## Generators { data-advanced="true" } + +A **generator** is a function that pauses and resumes instead of running start to finish and returning once. Calling it doesn't run the body — it returns a **generator object** that produces values one at a time, only as they're asked for. + +```python-ref +def species_generator(): + yield "ball" + yield "burmese" + yield "boa" + +for species in species_generator(): + print(species) +``` + +### Generator vs. a regular function { data-card-link="skip" } + +A regular function does all its work up front and returns one complete result; a generator pauses after each `yield` and resumes on request. + +```python-ref +def species_list(): # regular function + return ["ball", "burmese", "boa"] # builds the whole list before returning + +def species_generator(): # generator — has a yield in its body + yield "ball" # produces one value, pauses, resumes on the next request + yield "burmese" + yield "boa" +``` + +```python-ref +values = species_list() # values is a list +values[0] # "ball" — a list supports indexing +len(values) # 3 — and len() + +gen = species_generator() # gen is a generator object, not a list +gen[0] # TypeError — a generator supports neither +len(gen) # TypeError +``` + +| | Function returning a list | Generator | +|---|---|---| +| Hands back | The whole `list`, all at once, via `return` | A `generator` object, one value at a time, via `yield` | +| That result supports | Indexing, `len()`, looping more than once | Stepping forward once with `next()` or a `for` loop | +| Choose it when | The caller needs the whole result — to index into it, check its length, or reuse it more than once | Values are only ever read once, start to finish, or the full sequence is too large — or too open-ended — to hold in memory all at once | + +### yield vs return + +`return` exits a function and hands back one value, all at once. `yield` hands back one value but pauses the function in place, keeping its local variables intact — the next call resumes right after that `yield` instead of starting over. + +`next()` steps a generator forward one `yield` at a time. A `for` loop does this automatically, and stops cleanly on `StopIteration` instead of letting it raise. + +```python-ref +def species_generator(): + yield "ball" + yield "burmese" + +gen = species_generator() +next(gen) # "ball" +next(gen) # "burmese" +next(gen) # StopIteration — no values left +``` + +### Memory efficiency + +A generator produces values on demand instead of building the whole result up front, so it can represent a sequence too large to fit in memory — or one with no fixed end at all. + +`count_up()` never finishes and never stores more than the current `n` — a list built the same way (`[1, 2, 3, ...]`) would have to stop somewhere or run out of memory trying not to. + +```python-ref +def count_up(): + n = 1 + while True: + yield n + n += 1 + +counter = count_up() +next(counter) # 1 +next(counter) # 2 +``` + +### Generator expressions + +Parentheses instead of brackets turn a [list comprehension](collections.md#list-comprehension) into a generator expression — same filtering and transforming syntax, but values are produced lazily instead of built into a list all at once. + +`doubled_list` below is an actual `list` — `[10, 24, 16]`, every value already computed — so it supports indexing, `len()`, and looping over more than once. `doubled_gen` is a `generator` — nothing has been computed yet, and it only supports stepping forward once with `next()` or a `for` loop, the same [list vs. generator](#generator-vs-a-regular-function) tradeoff covered above. + +Use a list comprehension when the result needs indexing, `len()`, or more than one pass. Use a generator expression when it's only read once, or building the whole list would hold more in memory than necessary. + +```python-ref +doubled_list = [length * 2 for length in [5, 12, 8]] # builds the whole list right away +doubled_gen = (length * 2 for length in [5, 12, 8]) # computes each value only when asked +``` + +```python-ref +doubled_list[0] # 10 +len(doubled_list) # 3 + +next(doubled_gen) # 10 — computed on demand +doubled_gen[0] # TypeError — a generator doesn't support indexing +``` + +??? tip "Stopping early" + A generator expression can stop before producing every value — useful for finding just the first match without computing the rest. + + ```python-ref + first_long = next(s for s in ["ball", "burmese", "boa"] if len(s) > 5) # "burmese" — "boa" is never checked + ``` + + The equivalent list comprehension would build and check every item first, even though only the first one ends up used.
diff --git a/docs/index.md b/docs/index.md index 7ff3cce..1900dcc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -382,20 +382,29 @@ hide: Package a named block of code to run it at any time. [**`def`**](functions.md#defining-a-function): - [`default parameter values`](functions.md#default-parameter-values) + [`**kwargs`](functions.md#kwargs-dict) + [`*args`](functions.md#args-tuple) + [`defaults`](functions.md#default-values) [`docstrings`](functions.md#docstrings) - [`keep functions focused`](functions.md#keep-functions-focused) - [`keyword arguments`](functions.md#keyword-arguments) + [`parameters`](functions.md#parameters) + [`pass`](functions.md#pass-placeholder) [`return`](functions.md#return-values) + + [`combining *args and **kwargs`](functions.md#combining-regular-args-and-kwargs) + [`keyword-only`](functions.md#keyword-only) + [`positional-only`](functions.md#positional-only) [`type hints`](functions.md#type-hints) + {: data-advanced="true" } - [**`flexible arguments`**](functions.md#flexible-arguments): - [`**kwargs`](functions.md#kwargs) - [`*args`](functions.md#args) + [**`calling a function`**](functions.md#calling-a-function): + [`arguments`](functions.md#arguments) + [`keyword`](functions.md#by-keyword) + [`required`](functions.md#required) + [`return value`](functions.md#saving-the-return-value) + [`unpacking`](functions.md#unpacking) [**`scope`**](functions.md#scope): [`local vs global`](functions.md#local-vs-global-variables) - {: data-advanced="true" } [**`recursion`**](functions.md#recursion) {: data-advanced="true" } @@ -408,30 +417,58 @@ hide: [`wrapping`](functions.md#wrapping-the-call) {: data-advanced="true" } -- :material-package-variant:{ .lg .middle } [__Classes__](oop.md) + [**`generators`**](functions.md#generators): + [`generator expressions`](functions.md#generator-expressions) + [`memory`](functions.md#memory-efficiency) + [`yield`](functions.md#yield-vs-return) + {: data-advanced="true" } + +- :material-package-variant:{ .lg .middle } [__Classes__](classes.md) Bundle related values and functions to a reusable blueprint for similar objects. - [**`class`**](oop.md#classes-and-objects): - [`__init__()`](oop.md#the-__init__-method) - [`object methods`](oop.md#object-methods) - [`self`](oop.md#the-self-parameter) + [**`class`**](classes.md#defining-a-class): + [`__init__()`](classes.md#the-__init__-method) + [`class attributes`](classes.md#class-attributes) + [`instance attributes`](classes.md#instance-attributes) + [`methods`](classes.md#object-methods) + [`self`](classes.md#the-self-parameter) + + [**`method decorators`**](classes.md#method-decorators): + [`@classmethod`](classes.md#classmethod) + [`@property`](classes.md#property) + [`@staticmethod`](classes.md#staticmethod) + {: data-advanced="true" } + + [**`inheritance`**](classes.md#inheritance): + [`adding attributes and methods`](classes.md#adding-attributes-and-methods) + [`__init__()`](classes.md#overriding-__init__) + [`overriding`](classes.md#overriding-methods) + [`super()`](classes.md#using-super) - [**`method decorators`**](oop.md#method-decorators): - [`@classmethod`](oop.md#classmethod) - [`@property`](oop.md#property) - [`@staticmethod`](oop.md#staticmethod) + [`multiple inheritance`](classes.md#multiple-inheritance) {: data-advanced="true" } - [**`inheritance`**](oop.md#inheritance): - [`adding attributes and methods`](oop.md#adding-attributes-and-methods) - [`overriding __init__()`](oop.md#overriding-__init__) - [`overriding methods`](oop.md#overriding-methods) - [`super()`](oop.md#using-super) + [**`polymorphism`**](classes.md#polymorphism): + [`inheritance`](classes.md#polymorphism-via-inheritance) + [`duplicate method names`](classes.md#duplicate-method-names) + {: data-advanced="true" } + + [**`encapsulation`**](classes.md#encapsulation): + [`@property`](classes.md#controlled-access-with-property) + [`double underscore`](classes.md#double-underscore) + [`single underscore`](classes.md#single-underscore) + {: data-advanced="true" } + + [**`operator overloading`**](classes.md#operator-overloading): + [`__add__`](classes.md#arithmetic-with-__add__) + [`__eq__ and __lt__`](classes.md#comparing-with-__eq__-and-__lt__) + {: data-advanced="true" } + + [**`dataclasses`**](classes.md#dataclasses) + {: data-advanced="true" } - [**`polymorphism`**](oop.md#polymorphism): - [`polymorphism via inheritance`](oop.md#polymorphism-via-inheritance) - [`same method name, unrelated classes`](oop.md#same-method-name-unrelated-classes) + [**`abstract base classes`**](classes.md#abstract-base-classes) {: data-advanced="true" } diff --git a/docs/libraries/datetime.md b/docs/libraries/datetime.md index 76db70f..d54f81c 100644 --- a/docs/libraries/datetime.md +++ b/docs/libraries/datetime.md @@ -53,7 +53,7 @@ Pass the year, month, and day as plain integers to build a specific `date`. Usef observed = date(2026, 7, 23) # 2026-07-23 ``` -### Formatting with `strftime` +### Formatting with strftime Turns a `date` or `datetime` into a custom-formatted string. `strftime` ("string format time") — `%B` is the full month name, `%d` the zero-padded day, `%Y` the four-digit year. It's the standard way to control exactly how a date is displayed. @@ -114,7 +114,7 @@ last_seen = date(2026, 8, 6) last_seen - first_seen # timedelta(days=14) ``` -### Parsing a string with `strptime` +### Parsing a string with strptime The reverse of `strftime` — reads a date out of a string. `strptime` ("string parse time") takes the same format codes describing how that string is laid out. This is how a date typed by a user, or read from a CSV file, gets turned back into a real `datetime` you can do arithmetic on. diff --git a/docs/libraries/pillow.md b/docs/libraries/pillow.md index f15c524..f4d0051 100644 --- a/docs/libraries/pillow.md +++ b/docs/libraries/pillow.md @@ -322,7 +322,7 @@ img.save("shapes.png") ``` ??? tip "Drawing with objects" - Once a drawing gets complicated, it's common to wrap each thing you're drawing in its own class — an object that stores its own position/size/color, and knows how to draw itself given a drawing context. Nothing here is Pillow-specific: it's the same pattern covered in [Classes](../oop.md) — bundling data with the behavior that acts on it — just applied to a shape instead of a snake. A calling function loops over a list of these objects and calls `.draw()` on each, so building a complex image — dozens of randomly placed shapes, say, using the `random` module — is just a loop appending new `Shape` objects rather than dozens of manual `draw_context` calls. + Once a drawing gets complicated, it's common to wrap each thing you're drawing in its own class — an object that stores its own position/size/color, and knows how to draw itself given a drawing context. Nothing here is Pillow-specific: it's the same pattern covered in [Classes](../classes.md) — bundling data with the behavior that acts on it — just applied to a shape instead of a snake. A calling function loops over a list of these objects and calls `.draw()` on each, so building a complex image — dozens of randomly placed shapes, say, using the `random` module — is just a loop appending new `Shape` objects rather than dozens of manual `draw_context` calls. ```python-ref class Shape: diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 99f5922..8c94d69 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -869,6 +869,14 @@ input:checked + .md-consent__settings { margin-top: 0; } +/* Category titles read as a muted label over the card grid rather than + full-contrast body ink: lightened toward white on the cream light scheme, + dimmed toward black on the dark scheme (which otherwise inherits full + --pt-ink from the rule below). */ +[data-md-color-scheme="default"] .md-typeset .pt-category > h4.pt-homepage-heading { + color: color-mix(in srgb, var(--pt-ink) 80%, white); +} + /* "Add-On Libraries" heading — swap the default h1 margins (0 top / 2.5rem bottom) so the space sits above it instead, separating it from the core grid and pulling it tight to the first library row. */ @@ -934,7 +942,7 @@ input:checked + .md-consent__settings { card title, description, and the bold lead keyword in each tag row — is the off-white --pt-ink. The light scheme keeps the original green title + tags. */ [data-md-color-scheme="slate"] .md-typeset .pt-category .pt-homepage-heading { - color: var(--pt-ink); + color: color-mix(in srgb, var(--pt-ink) 80%, black); } [data-md-color-scheme="slate"] .md-typeset .pt-category .grid.cards > ul > li > p:first-child > .twemoji { @@ -1232,6 +1240,10 @@ input:checked + .md-consent__settings { color: var(--pt-heading-h3); } +.md-typeset h4 { + color: var(--pt-heading-h3); +} + .md-typeset .pt-fake-h2 { font-family: "Cormorant Garamond", serif; font-weight: 700; diff --git a/includes/glossary.md b/includes/glossary.md index 3e5fc0c..9aa7595 100644 --- a/includes/glossary.md +++ b/includes/glossary.md @@ -63,3 +63,8 @@ *[Graceful]: Handling a failure without crashing or losing data — continuing on, showing a clear message, or falling back to a default instead of stopping abruptly *[gracefully]: In a way that handles a failure without crashing or losing data — continuing on, showing a clear message, or falling back to a default instead of stopping abruptly *[Gracefully]: In a way that handles a failure without crashing or losing data — continuing on, showing a clear message, or falling back to a default instead of stopping abruptly +*[lazy]: Computing or producing a value only at the moment it's actually needed, instead of all at once ahead of time +*[Lazy]: Computing or producing a value only at the moment it's actually needed, instead of all at once ahead of time +*[lazily]: In a way that computes or produces a value only at the moment it's actually needed, instead of all at once ahead of time +*[Lazily]: In a way that computes or produces a value only at the moment it's actually needed, instead of all at once ahead of time +*[PascalCase]: Capitalizing each word with no separators (e.g. Snake, BallPython) — the naming convention for classes, unlike variables' snake_case diff --git a/mkdocs.yml b/mkdocs.yml index 96862a6..7fe9411 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,7 +13,7 @@ nav: - Conditionals: conditionals.md - Loops: loops.md - Functions: functions.md - - Classes: oop.md + - Classes: classes.md - Modules: modules.md - Files: files.md - Style: style.md