Python Programming
40 subscribers
5 photos
2 links
🐍 Python Programming | Code. Learn. Grow.

Welcome to Python Programming — your go-to Telegram community for mastering Python, from basics to advanced!
Download Telegram
Channel name was changed to «Python»
Channel name was changed to «Python Learning»
Channel photo updated
Channel name was changed to «Python Programming»
Live stream started
Channel photo updated
🚀 Boost your file handling with pathlib!

Tired of fiddling with string concatenations for paths? pathlib lets you treat file paths like objects – safe, readable, and cross‑platform.

from pathlib import Path

# 📁 Build a path to "data/report.txt" inside the user's home folder
home = Path.home()
report_path = home / "data" / "report.txt"

# ✅ Check if it exists, read its content, and print the first line
if report_path.is_file():
first_line = report_path.read_text().splitlines()[0]
print(f"🗒️ First line: {first_line}")
else:
print("❌ File not found!")


Why you’ll love it:
- No more os.path.join headaches.
- Works the same on Windows, macOS, Linux.
- Handy methods like .is_file(), .read_text(), .write_text() keep code clean.

Give it a try and say goodbye to brittle path strings! 🎉
💡 Quick tip: the Walrus operator (:=) lets you assign and test a value in a single line. Great for reading input, looping over data, or any situation where you’d otherwise need two statements.

# Read numbers until a non‑numeric line appears
while (line := input("Enter a number (or anything else to stop): ")) .isdigit():
number = int(line)
print(f"✅ You entered {number}")

print("🚪 Done!")


What’s happening?
- line := input(...) captures the user’s input.
- The same expression is immediately checked with .isdigit().

No extra line = input(...) line, no duplicate calls—cleaner and faster to write. Give it a try in your next script! 🚀
💡 Silently ignore the errors you expect – without a try/except clutter

When you have a piece of code that may raise a known exception (e.g., trying to delete a file that might already be gone), wrapping it in a full try/except block can feel noisy.
contextlib.suppress does the same thing in a single line and keeps the flow readable.

from contextlib import suppress
import os

# Remove a temp file if it exists – ignore the FileNotFoundError otherwise
with suppress(FileNotFoundError):
os.remove('temp_data.json')


✅ Why you’ll love it
- No extra indentation levels – your main logic stays front‑and‑center.
- Explicit about which exception is being ignored, so it’s safe and self‑documenting.
- Works with any exception type, even custom ones.

Give it a try next time you need a “try‑and‑forget” cleanup! 🚀
❤1
🚀 One‑liner to count & rank items in a list

Ever needed a quick frequency report (e.g., most used words, top error codes) without writing a loop? 🎯 collections.Counter does it in a single line and gives you handy helpers like most_common.

from collections import Counter

data = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']
top = Counter(data).most_common(2) # → [('apple', 3), ('banana', 2)]

print(top)


Why it’s cool
- Fast – implemented in C, O(n) time.
- Readable – the intent is obvious at a glance.
- Flexible – most_common() can return all items or just the top N.

Next time you need a quick tally, reach for Counter and skip the manual dictionary gymnastics! 🌟
💡 Merge dictionaries in one clean line – the | operator (Python 3.9+)

When you need a base config and want to overlay user‑provided values, the classic dict.update() mutates the original. With the new pipe syntax you can keep both dictionaries untouched and get a brand‑new combined mapping.

# default settings
default_cfg = {
"host": "localhost",
"port": 8000,
"debug": False,
}

# values supplied by the user (maybe from a CLI or a JSON file)
user_cfg = {
"port": 8080,
"debug": True,
}

# 👇 merge → user_cfg wins on overlapping keys
merged_cfg = default_cfg | user_cfg
print(merged_cfg)
# {'host': 'localhost', 'port': 8080, 'debug': True}


Want to update in place? Use the in‑place version:

default_cfg |= user_cfg   # modifies default_cfg


No loops, no copy() calls – just a single, readable operator. Perfect for settings, API payloads, or any situation where you blend two maps. 🚀
💡 Keep the order, drop the duplicates – in one line

Got a list with repeating items and need the first occurrence of each element, preserving the original order?
Python’s dict (since 3.7) remembers insertion order, so you can turn the sequence into keys and back again:

>>> data = ["apple", "banana", "apple", "orange", "banana", "grape"]
>>> unique = list(dict.fromkeys(data))
>>> unique
['apple', 'banana', 'orange', 'grape']


* How it works
* dict.fromkeys(data) creates a dict where each list item becomes a key – duplicates are automatically discarded.
* Converting the dict back to a list (list(...)) gives you the values in the exact order they first appeared.

🔧 When to use: cleaning up CSV columns, de‑duplicating user input, or any place you need “unique but ordered” without pulling in OrderedDict or extra loops.

Give it a try – it’s both fast and readable! 🚀
🚀 Dataclasses + mutable defaults – the hidden trap and the clean fix

If you’ve ever written a class like this:

class Bag:
def __init__(self, items=[]):
self.items = items


you probably noticed a weird behavior: every new Bag() shares the same list. Adding an item to one bag magically appears in all the others. 😱

Why does this happen?

In Python, default argument values are evaluated only once – at function definition time. The empty list [] is created a single time and then stored in the function’s __defaults__. Every call that doesn’t supply its own items argument re‑uses that very same list object. The same rule applies to dataclasses because their generated __init__ works just like a normal function.

The dataclass‑friendly solution: field(default_factory=…)

dataclasses.field lets you specify a factory – a callable that is invoked each time a new instance is created. The factory returns a fresh object, so every instance gets its own independent mutable container.

from dataclasses import dataclass, field

@dataclass
class Bag:
# ✅ each Bag gets its own list
items: list = field(default_factory=list)
# you can also use other factories, e.g. set() or dict()


How it works under the hood
- When the dataclass decorator builds Bag.__init__, it inserts items: list = field(default_factory=list).
- At call time, default_factory() is executed, producing a brand‑new list.
- The generated __init__ assigns that fresh list to self.items.

Because the factory is a callable, you can pass any function that returns the desired mutable type, even a lambda that captures external state.

Real‑world use case: building a tree of nodes

Imagine you’re modeling a simple filesystem:

from __future__ import annotations
from dataclasses import dataclass, field

@dataclass
class Node:
name: str
children: list[Node] = field(default_factory=list)

def add_child(self, child: Node) -> None:
self.children.append(child)

# Build a tiny hierarchy
root = Node("root")
root.add_child(Node("etc"))
root.add_child(Node("var"))
print(root)


If you had used children=[] directly, every Node would share the same children list, and adding a child to one node would magically appear under all others – a disaster for tree structures.

Edge case: custom factories with arguments

Sometimes you need a default that isn’t just an empty container, e.g. a list pre‑filled with a timestamp:

import datetime
from dataclasses import dataclass, field

def timestamped_list():
# each instance gets its own list containing the creation time
return [datetime.datetime.utcnow()]

@dataclass
class EventLog:
entries: list[datetime.datetime] = field(default_factory=timestamped_list)

log1 = EventLog()
log2 = EventLog()
print(log1.entries, log2.entries) # different timestamps


Because the factory is called every time a new EventLog is instantiated, each list carries its own unique data.

Quick checklist ✅

- Never use a mutable object ([], {}, set()) directly as a default value in a dataclass or function.
- Use field(default_factory=…) for dataclasses.
- For functions, replace the mutable default with None and create the object inside the body (if arg is None: arg = []).
- Remember: the factory can be any zero‑argument callable, giving you full flexibility.

💡 Takeaway: The default_factory pattern gives you the same convenience as a simple default argument, but without the shared‑state surprise. Your dataclasses will behave predictably, and you’ll avoid a whole class of subtle bugs that even seasoned developers stumble over. Happy coding! 🚀
Hey Python newbies! 🌟
Let’s talk about something that makes Python look clean and tidy: indentation.

In many languages you need special symbols ({ } or end) to show where a block of code starts and stops. Python chose a different path – it uses spaces (or tabs) at the beginning of a line. This isn’t just for looks; it tells the computer which lines belong together.

Why does indentation matter?

1. It defines “blocks”.
A block is a group of lines that should be executed together.
- When you write an if statement, everything indented under it runs only if the condition is true.
- The same goes for loops (for, while) and functions.

2. It prevents mistakes.
Because the structure is visible, you can spot missing or extra spaces quickly.
If the indentation is wrong, Python will raise an IndentationError and tell you exactly where the problem is.

3. It keeps code readable.
When you (or anyone else) open the file later, the visual layout instantly shows the logic flow.

How does it work?

- Choose either spaces or a tab for the whole file – don’t mix them.
- The most common practice (and what the official style guide, PEP 8, recommends) is 4 spaces per level.
- Every time you start a new block, add one more level of indentation; when the block ends, go back to the previous level.

Let’s see a real example – a tiny program that asks for a number and tells you if it’s even or odd:

# Ask the user for a number
number_str = input("Enter a whole number: ")

# Convert the text we got into an actual integer (a number)
number = int(number_str)

# Check if the number is even or odd
if number % 2 == 0: # % is the remainder operator
print(f"{number} is **even**.") # This line is inside the if‑block
else:
print(f"{number} is **odd**.") # This line is inside the else‑block


What’s happening step‑by‑step?

1. input() shows a prompt and waits for you to type something. It always returns text (str).
2. int() changes that text into a real number we can do math with.
3. if number % 2 == 0: checks the remainder when number is divided by 2.
- If the remainder is 0, the condition is True and Python runs the line that is indented right under the if.
- If not, it jumps to the else: part and runs the indented line there.

Notice how the two print() statements line up under their respective if and else. That alignment is the indentation telling Python, “These lines belong to these blocks.”

Quick checklist for clean indentation

- Consistent width: 4 spaces each level (no tabs unless you set your editor to insert 4 spaces for a tab).
- Never mix spaces and tabs in the same file.
- Indent after a colon (:) – this includes if, elif, else, for, while, def, class, etc.
- Dedent (remove one level) when the block ends.

Try it yourself!

1. Open a new file called even_odd.py.
2. Copy the code above, paying special attention to the spaces before the print() lines.
3. Run it with python even_odd.py and test a few numbers.

If you get an IndentationError, look at the line number Python mentions – the spaces are probably off.

Bottom line:
Indentation isn’t a cosmetic detail; it’s the language’s way of understanding your logic. Mastering it early will make every Python concept feel clearer and your code look beautiful. Keep an eye on those four spaces, and you’ll never get lost in a sea of braces again! 🚀
🚀 Shrink your objects, boost attribute look‑ups – unlock the power of __slots__

If you’ve ever profiled a long‑running service and saw hundreds of megabytes tied up in plain Python objects, the culprit is probably the default per‑instance __dict__. Every time you instantiate a class, Python creates a tiny dictionary to store its attributes – handy, but memory‑hungry.

__slots__ tells the interpreter: “Hey, this class will only ever have these exact attributes – don’t waste a dict, lay them out in a compact C‑struct instead.”

Why it works

* No per‑instance __dict__ – the object’s memory layout is fixed at class creation, so the interpreter can allocate a static block of memory sized exactly for the declared slots.
* Faster attribute access – the lookup becomes a simple offset calculation rather than a hash‑table probe.
* Predictable memory footprint – each instance occupies the same number of bytes, which is great for large collections (think millions of rows or cache entries).

A minimal example

# classic class – each instance carries a __dict__
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y

# slot‑based version – no __dict__ per instance
class PointSlots:
__slots__ = ('x', 'y') # declare the only allowed attributes

def __init__(self, x: float, y: float):
self.x = x
self.y = y


Seeing the difference

import sys, timeit

p = Point(1.0, 2.0)
ps = PointSlots(1.0, 2.0)

print('size with __dict__: ', sys.getsizeof(p.__dict__) + sys.getsizeof(p))
print('size with __slots__: ', sys.getsizeof(ps))

# Rough speed test – attribute read
n = 10_000_000
print('dict read :',
timeit.timeit('p.x', globals={'p': p}, number=n))
print('slots read :',
timeit.timeit('ps.x', globals={'ps': ps}, number=n))


Typical output on CPython 3.11 (64‑bit):

size with __dict__:    72
size with __slots__: 56
dict read : 0.88 s
slots read : 0.62 s


The numbers vary by platform, but the trend is clear – slots shave off memory and give a modest speed bump on attribute access.

Common pitfalls & how to avoid them

| Pitfall | Explanation | Fix |
|---|---|---|
| Trying to add a new attribute at runtime | obj.new_attr = 5 raises AttributeError because the slot list is closed. | If you need occasional dynamic fields, add '__dict__' to __slots__: __slots__ = ('x', 'y', '__dict__'). |
| Multiple inheritance with slots | Each base class must define its own __slots__; otherwise you get a TypeError. | Use a single base that defines all slots, or explicitly combine them: class C(A, B): __slots__ = A.__slots__ + B.__slots__. |
| Pickling | __slots__ objects are picklable, but you must ensure the slots are present in __getstate__/__setstate__ if you override them. | Usually no extra work; only needed for custom state handling. |
| Dataclasses | Before Python 3.10 you needed a manual workaround. | From Python 3.10: @dataclass(slots=True) creates a dataclass that uses slots automatically. |

When to use __slots__

* Large homogeneous collections – e.g. a list of millions of coordinates, AST nodes, or cache entries.
* Performance‑critical loops where attribute look‑ups dominate.
* Memory‑constrained environments such as micro‑services or embedded Python (MicroPython supports a variant of slots).

Avoid __slots__ for objects that need a flexible, dynamic attribute set, or when you rely heavily on __dict__‑based introspection (e.g. generic serialization frameworks that expect a dict).

A quick recipe for modern code

(continued below 👇)
from __future__ import annotations
from dataclasses import dataclass

# Python 3.10+ – clean, typed, and slot‑enabled in one line
@dataclass(slots=True, frozen=True)
class User:
id: int
username: str
is_active: bool = True

# Usage
u = User(42, 'alice')
print(u.username) # attribute access is still normal
# u.email = 'a@b.com' # ❌ raises AttributeError – slots protect us


The frozen=True flag makes the instance immutable (adds __setattr__ protection), while slots=True gives you the memory‑savings without any extra boilerplate.

Bottom line
If you’re past the “hello‑world” stage and your program is handling lots of objects, adding __slots__ is a low‑effort, high‑payoff optimisation. It forces a stricter design (explicit attribute list), reduces memory churn, and can make tight loops a bit snappier.

Give it a try on a hot path, measure with sys.getsizeof and timeit, and you’ll see the difference in real time. Happy slot‑ting! 🎯
🚀 One‑Sitting Python Project: “Daily‑Habit Tracker” (CLI + JSON)

Ever feel like you’re just missing a tiny tool to log whether you did your habit today? Build a single‑file command‑line habit tracker that lets you:

1. Add a new habit (e.g., “meditate”, “read 20 min”).
2. Mark it as done for today (or any past date).
3. Show a quick summary – “7 / 30 days” streak, total completions, etc.

All of this fits comfortably into a single sitting (≈ 30 min) and teaches you a handful of core Python ideas that are reusable in countless other scripts.

Why this works – the concepts behind the magic

| Concept | What it gives you | Why it matters |
|--------|-------------------|----------------|
| argparse | Friendly CLI interface (add, done, report). | Turns a plain script into a real command‑line tool without extra dependencies. |
| pathlib.Path | Platform‑independent file handling. | Guarantees your habits.json lives next to the script on Windows, macOS and Linux. |
| json | Human‑readable persistent storage. | You get a tiny database that you can open in a text editor, version‑control, or share. |
| datetime | Date objects, ISO formatting. | Enables reliable “today”, “yesterday”, or any custom date you want to log. |
| collections.defaultdict | Auto‑creates nested dictionaries. | No need to check “if key exists” – the structure builds itself as you log. |
| rich (optional) | Nice coloured tables & progress bars. | Makes the output feel like a polished app, not just print statements. |

> Tip: If you don’t have rich installed, pip install rich – otherwise the script works fine without it.

Sketch of the approach

1. Load / create a JSON file (habits.json).
2. Parse the command‑line arguments (add, done, report).
3. Mutate the in‑memory dict:
* add → insert an empty habit entry.
* done → set data[habit][date] = True.
* report → compute totals & streaks.
4. Write back the JSON (pretty‑printed) so the next run sees the updates.

All heavy lifting is done by the standard library except for optional pretty output.

Quick code glimpse

(continued below 👇)
#!/usr/bin/env python3
import argparse, json, sys
from collections import defaultdict
from datetime import date, datetime, timedelta
from pathlib import Path

# ── Optional fancy output ─────────────────────────────────────────────────────
try:
from rich.table import Table
from rich.console import Console
console = Console()
def fancy_report(stats):
tbl = Table(title="📊 Habit Summary")
tbl.add_column("Habit")
tbl.add_column("Streak", justify="right")
tbl.add_column("Done / Total", justify="right")
for h, (streak, done, total) in stats.items():
tbl.add_row(h, str(streak), f"{done} / {total}")
console.print(tbl)
except Exception: # rich not installed – fall back to plain prints
console = None
def fancy_report(stats):
for h, (streak, done, total) in stats.items():
print(f"{h:20} Streak: {streak:2} Done/Total: {done}/{total}")

# ── Helper functions ────────────────────────────────────────────────────────
DATA_FILE = Path(__file__).with_name("habits.json")

def load_data() -> defaultdict:
if DATA_FILE.exists():
with DATA_FILE.open() as f:
raw = json.load(f)
# Convert nested dicts back to defaultdicts for auto‑creation
return defaultdict(lambda: defaultdict(bool), {
habit: defaultdict(bool, entries) for habit, entries in raw.items()
})
return defaultdict(lambda: defaultdict(bool))

def save_data(data: defaultdict):
# Turn defaultdicts into normal dicts for JSON serialisation
serialisable = {h: dict(days) for h, days in data.items()}
DATA_FILE.write_text(json.dumps(serialisable, indent=2, sort_keys=True))

def mark_done(data, habit, when):
iso = when.isoformat()
data[habit][iso] = True
print(f"✅ Marked '{habit}' as done on {iso}")

def streak_for(habit_days):
"""Longest consecutive streak ending today."""
today = date.today()
streak = 0
while habit_days.get(today.isoformat()):
streak += 1
today -= timedelta(days=1)
return streak

def generate_report(data):
stats = {}
for habit, days in data.items():
total = len(days)
done = sum(1 for v in days.values() if v)
streak = streak_for(days)
stats[habit] = (streak, done, total)
fancy_report(stats)

# ── CLI handling ───────────────────────────────────────────────────────────
parser = argparse.ArgumentParser(
description="🗓️ Simple command‑line habit tracker"
)
sub = parser.add_subparsers(dest="cmd", required=True)

add_p = sub.add_parser("add", help="Register a new habit")
add_p.add_argument("habit", help="Name of the habit")

done_p = sub.add_parser("done", help="Mark habit as completed today")
done_p.add_argument("habit", help="Habit to mark")
done_p.add_argument(
"-d", "--date",
type=lambda s: datetime.strptime(s, "%Y-%m-%d").date(),
default=date.today(),
help="Date in YYYY‑MM‑DD (default: today)"
)

sub.add_parser("report", help="Show a summary table")

args = parser.parse_args()
db = load_data()

if args.cmd == "add":
if args.habit in db:
sys.exit(f"⚠️ Habit '{args.habit}' already exists.")
db[args.habit] # default dict creates empty entry
print(f"🆕 Added habit: {args.habit}")

elif args.cmd == "done":
if args.habit not in db:
sys.exit(f"❓ Unknown habit '{args.habit}'. Add it first.")
mark_done(db, args.habit, args.date)

elif args.cmd == "report":
generate_report(db)

save_data(db)


What’s happening under the hood?

* defaultdict removes the need for “if habit not in data: data[habit] = {}” – the container auto‑creates an empty dict the moment you access a new key.
*
Hey Python newbies! 🌱
Ever felt like you’re “walking blind” through a list and wish you could see where you are at each step? Let’s fix that with a tiny, super‑useful tool called enumerate.

🎯 What problem does it solve?

You have a list and you want two things at the same time:

1. The item itself (e.g., a word, a number, a name).
2. The position of that item (its index – the “0, 1, 2…” number).

The classic way is to use a counter variable and manually increase it:

i = 0
for fruit in fruits:
print(i, fruit)
i += 1


That works, but it’s a bit clunky and easy to forget the i += 1 line.

🛠️ Enter enumerate

enumerate is a built‑in function that automatically pairs each item with its index. It returns an iterator of tuples (index, item).

Why it works:
Under the hood, enumerate creates a tiny object that keeps an internal counter. Each time the loop asks for the next element, it returns the current counter value and the next item from the original iterable, then bumps the counter by one. No extra variables needed!

📚 How to use it

fruits = ["apple", "banana", "cherry", "date"]

for idx, fruit in enumerate(fruits):
print(f"Item {idx}: {fruit}")


Output

Item 0: apple
Item 1: banana
Item 2: cherry
Item 3: date


Key points

- idx (or any name you like) receives the index.
- fruit receives the actual list element.
- The index starts at 0 by default – that’s Python’s way of counting from zero.

If you’d rather start counting from 1 (maybe for user‑facing messages), just give enumerate a second argument:

for idx, fruit in enumerate(fruits, start=1):
print(f"{idx}. {fruit}")


Output:

1. apple
2. banana
3. cherry
4. date


🚀 When is enumerate a game‑changer?

- Finding the position of a specific value – you can break out of the loop as soon as you spot what you need.
- Creating numbered menus – perfect for command‑line interfaces.
- Debugging – quickly see “which element broke my code” without extra prints.

📌 Quick cheat‑sheet

- Syntax: enumerate(iterable, start=0)
- Returns: an iterator of (index, item) tuples
- Common use: for i, x in enumerate(some_list): …

Give it a try!
Take any list you already have (numbers, strings, even a list of dictionaries) and rewrite the loop with enumerate. You’ll see how clean and readable your code becomes.

Happy coding, and remember: small helpers like enumerate make big differences. 🚀✨
🔍 Dive Deep: Context Variables — the secret sauce for clean async state management

When you start mixing asyncio tasks, you quickly notice a recurring pain point: how do you keep per‑task state without leaking it into other coroutines?
The usual suspects – globals, thread‑locals, or even passing arguments around – either break the async model or make the code noisy.

Enter contextvars (Python 3.7+). It gives you task‑local storage that propagates automatically across await points, while staying isolated between concurrent tasks. Let’s unpack why it works, and how to wield it like a pro.

📚 The why – what makes contextvars tick?

1. Isolation at the coroutine level
A Context is a snapshot of all active ContextVar values. Each Task holds its own Context, created from the parent’s snapshot at the moment the task is scheduled. This means any change a task makes to a variable stays local to that task.

2. Transparent propagation through await
When you await a coroutine, the interpreter automatically switches to the awaiting task’s Context. No manual plumbing required – the same variable you read/write before the await will resolve to the same value after it.

3. Explicit reset & token semantics
Setting a ContextVar returns a token that lets you roll back the change (var.reset(token)). This is crucial for temporary overrides (think request‑scoped data in a web server) without polluting the surrounding context.

4. Compatibility with asyncio primitives
TaskGroup, gather, create_task, and even run_in_executor respect the current Context, so your state travels safely across thread pools as well.

⚙️ The how – practical patterns

1️⃣ Basic usage – a request‑id logger

import asyncio
import contextvars
import random
import time

# A ContextVar that will hold a unique request identifier
request_id = contextvars.ContextVar("request_id", default="unknown")

def log(msg: str):
"""Simple logger that prefixes the current request_id."""
rid = request_id.get()
print(f"[{time.strftime('%X')}] [{rid}] {msg}")

async def handler(name: str):
# Assign a fresh id for this handler – isolated per task
token = request_id.set(f"{name}-{random.randint(1000, 9999)}")
log("started")
await asyncio.sleep(random.random() * 0.3)
log("still running")
# Clean up – optional but good practice if you reuse the same ContextVar elsewhere
request_id.reset(token)
log("finished")

async def main():
await asyncio.gather(handler("alpha"), handler("beta"), handler("gamma"))

asyncio.run(main())


Why it works:
- Each handler gets its own snapshot of request_id.
- The set call only affects the current task’s Context; other tasks keep their own values.
- When we reset with the token, we restore the previous value (or the default), preventing accidental bleed‑through if the same coroutine is reused later.

2️⃣ Propagating context into a thread pool

import concurrent.futures
import contextvars

user = contextvars.ContextVar("user", default="anonymous")

def blocking_io():
# Even though we’re on a thread, the current Context is copied automatically
return f"processing for {user.get()}"

async def main():
user.set("alice")
loop = asyncio.get_running_loop()
with concurrent.futures.ThreadPoolExecutor() as pool:
result = await loop.run_in_executor(pool, blocking_io)
print(result) # → processing for alice


Why it works:
run_in_executor captures the current Context and injects it into the worker thread. The thread sees the same user value, but other threads get their own copies, preserving isolation.

3️⃣ Temporary overrides with ContextVar.run

(continued below 👇)