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
🚀 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 👇)
Sometimes you need a one‑off context change without messing with tokens:

def compute():
return f"value is {request_id.get()}"

# Run compute() with a temporary request_id
print(request_id.run(lambda: "temp-42", compute))
# → value is temp-42


ContextVar.run creates a new Context where the variable is set to the supplied value, executes the callable, then discards that Context. It’s a clean, functional‑style alternative to manual token handling.

🛡️ Common pitfalls & how to avoid them

- Never store mutable objects directly in a ContextVar unless you intend to share them.
The Context only copies the reference; mutating the object will be seen by every task that holds the same reference. Use immutable types (str, int, tuple) or deep‑copy if needed.

- Don’t forget the default!
If you access a ContextVar before any set, you’ll get a LookupError. Supplying a default= value sidesteps the exception and makes the variable safely readable everywhere.

- Be wary of contextvars.copy_context() in libraries.
Some third‑party tools (e.g., tracing libraries) clone the Context manually. If you rely on side‑effects inside those clones, you might get surprising divergence. Stick to the high‑level API (set, reset, run) unless you truly need a snapshot.

🎯 TL;DR – when to reach for contextvars

- Per‑request or per‑task metadata (request IDs, user sessions, locale) in async servers.
- Thread‑pool boundaries where you still need logical request scoping.
- Temporary overrides for testing or sandboxed execution.

Bottom line: contextvars gives you structured, leak‑free state that travels with your async flow, without the boilerplate of passing arguments everywhere. Master it, and your concurrent code will stay tidy, debuggable, and future‑proof.

Happy coding! 🚀
🚀 One‑Sitting Python Project: “Markdown Link Linter”

Ever opened a markdown file and wondered whether the links you’ve pasted are still alive? A tiny CLI that walks through a folder, pulls out every http(s)://… reference, pings each URL, and spits out a clean report can save you hours of broken‑link hunting. It fits comfortably in a single sitting, yet touches several core Python ideas.

Why it’s useful
- Keeps documentation trustworthy → no 404 surprises for readers.
- Works on any repo – just point it at the root folder.
- Gives you a quick “what’s broken” checklist you can commit back to the repo.

Core concepts you’ll practice

- Pathlib for OS‑independent file discovery (Path.rglob('*.md')).
- Regular expressions to extract URLs from raw text (re.findall).
- Requests (or the lighter http.client) to perform a HEAD request – we only need the status line, not the whole page, which makes the check fast and polite.
- Set to deduplicate URLs before hitting the network.
- ThreadPoolExecutor (from concurrent.futures) to parallelise the I/O‑bound checks without diving into async‑await.

The “why” behind the choices

- Pathlib abstracts away Windows vs. POSIX path quirks, letting you write for md in Path('.').rglob('*.md'): and be confident it works everywhere.
- A HEAD request returns just the headers (including the status code). If a server refuses HEAD, we gracefully fall back to a lightweight GET with stream=True and immediately close the connection – this avoids downloading megabytes of data.
- Using a set removes duplicate links early, which cuts network traffic dramatically, especially in large docs that reuse the same reference.
- ThreadPoolExecutor gives you simple concurrency: each thread waits on network I/O, so a pool of 10‑20 threads can finish a hundred checks in a blink, without the boilerplate of asyncio and aiohttp.

A minimal skeleton to spark you

import re
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests

# 1️⃣ Gather markdown files
md_files = list(Path('.').rglob('*.md'))

# 2️⃣ Pull out URLs (simple but effective)
url_pat = re.compile(r'https?://[^\s)>\]\'"]+')
all_urls = {match for f in md_files for match in url_pat.findall(f.read_text())}

def check_url(url: str) -> tuple[str, int]:
"""Return (url, status_code). Try HEAD first, fall back to GET."""
try:
r = requests.head(url, allow_redirects=True, timeout=5)
if r.status_code >= 400: # some sites dislike HEAD
r = requests.get(url, stream=True, timeout=5)
return url, r.status_code
except requests.RequestException:
return url, 0 # 0 signals a network error

# 3️⃣ Parallel check
broken = []
with ThreadPoolExecutor(max_workers=15) as pool:
future_to_url = {pool.submit(check_url, u): u for u in all_urls}
for fut in as_completed(future_to_url):
url, code = fut.result()
if code >= 400 or code == 0:
broken.append((url, code))

# 4️⃣ Report
print("\n🔎 Link check complete!\n")
if broken:
print("❌ Broken links found:")
for u, c in broken:
status = "no response" if c == 0 else f"HTTP {c}"
print(f" • {u} → {status}")
else:
print("✅ All links are reachable.")


How to expand it (optional, not required for the one‑sitting version)

- Output a Markdown table or JSON file so CI can fail on broken links.
- Add a --exclude flag to ignore certain domains (e.g., localhost).
- Integrate with GitHub Actions to run automatically on PRs.

(continued below 👇)
Give it a spin: copy the script into link_linter.py, pip install requests, then run python link_linter.py from the root of any docs repo. In under five minutes you’ll have a living audit of every hyperlink. Happy hacking! 🌟
👋 Hey Python newcomers!
Ever wanted to walk through a list and know where you are in that list at the same time?
Most beginners start with something like:

for i in range(len(my_list)):
item = my_list[i]
# do something with i and item


It works, but it’s a bit clunky and can lead to hidden bugs. Let’s unpack why enumerate is the friend you’ve been missing, and how it makes your code cleaner, safer, and easier to read.

📚 What’s really happening?

- range(len(my_list)) creates a new sequence of numbers from 0 up to len(my_list)-1.
- Every iteration the interpreter looks up my_list[i] by index.
- If you ever change the list length inside the loop (e.g., my_list.append(...)), the range you built doesn’t know about that change – it will still run the original number of times, which can cause IndexError or silently skip items.

enumerate solves this by turning the list itself into an iterator that yields pairs: (index, value). The iterator walks the list exactly as it exists at that moment, respecting any modifications (though you usually shouldn’t modify while iterating).

✅ The tidy way with enumerate

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

for idx, fruit in enumerate(fruits):
print(f"#{idx + 1}: {fruit}")


What you see:

- idx automatically starts at 0 (you can change the start with a second argument).
- fruit is the actual element at that position.
- No manual indexing, no extra len() call, no risk of mismatched lengths.

Why it works:
enumerate internally keeps a counter and, for each element it pulls from the original iterable, it returns a tuple (counter, element). The for loop then unpacks that tuple into idx and fruit.

🎯 When to use it

- Printing a numbered list (like the example above).
- Processing items while remembering their position – e.g., marking the first occurrence of a duplicate.
- When you need both index and value for calculations, without extra look‑ups.

⚠️ Common pitfalls (and how enumerate avoids them)

Mistake you might make — What goes wrong — How enumerate helps

Using range(len(seq)) and then changing seq inside the loop — Loop runs the old length → IndexError or skipped items — enumerate walks the current state of the iterable
Forgetting to add +1 for human‑readable numbering — Output starts at 0, which looks odd to users — You can pass a start value: enumerate(seq, start=1)
Manually managing two counters (e.g., i = 0; for x in seq: i += 1) — Easy to forget to increment, leads to off‑by‑one bugs — enumerate does the counting for you, no extra state

🛠️ Quick tip: start counting at 1

If you want the first item to be numbered 1 instead of 0, just give enumerate a start value:

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


Output:

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


📌 TL;DR (Take‑away)

- enumerate = clean, safe, and readable way to get both index and value while looping.
- It avoids manual index maths, reduces error‑prone code, and works with any iterable (lists, strings, tuples, even generators).
- Remember the optional start argument for human‑friendly numbering.

Give it a spin in your next script, and you’ll wonder how you ever lived without it! 🚀

Happy coding! 🎉
⚡️ Unlock High‑Performance Memoization for Any Argument Type

If you’ve been using functools.lru_cache you already know how sweet caching can be for pure functions.
But there’s a hidden snag: lru_cache only works with hash‑able arguments. As soon as you pass a list, dict, or a custom mutable object you’ll hit a TypeError.

Below is a drop‑in replacement that lets you cache any arguments – even unhashable ones – while still giving you the same LRU semantics, thread‑safety, and cache‑statistics you love.

🔍 How it works under the hood

1. Normalization – Every argument is transformed into an immutable, hash‑able representation.
* For built‑ins we recursively convert list → tuple, dict → frozenset of key/value pairs, set → frozenset.
* For custom objects we fall back to their id (the memory address) – safe because the object’s identity never changes during a single call.

2. Key construction – The normalized positional arguments and a sorted tuple of normalized keyword arguments are concatenated into a single tuple key.
This guarantees that (a=1, b=2) and (b=2, a=1) map to the same cache entry.

3. LRU mechanics – We reuse functools._lru_cache_wrapper logic via functools.lru_cache on an inner helper that receives the normalized key.
The wrapper handles the doubly‑linked list, hit/miss counters, and thread lock – we don’t have to reinvent that part.

4. Cache inspection – The wrapper still exposes .cache_info() and .cache_clear(), so you can monitor usage exactly like the std‑lib version.

💡 When to reach for this

* Functions that accept large data structures (e.g., pandas DataFrames, nested dicts) but are pure (no side‑effects).
* Recursive algorithms where the state is a mutable container – think DP on grids stored as lists of lists.
* APIs that receive JSON‑like payloads and would benefit from memoizing expensive validation or transformation steps.

🛠️ Full implementation

(continued below 👇)
import functools
import threading
from collections.abc import Mapping, Sequence, Set

def _make_hashable(obj):
"""Recursively turn mutable containers into immutable, hash‑able equivalents."""
if isinstance(obj, (str, bytes, int, float, bool, type(None))):
return obj # already hashable
if isinstance(obj, Mapping):
# Sort items to make order irrelevant
return frozenset((k, _make_hashable(v)) for k, v in sorted(obj.items()))
if isinstance(obj, (Set, frozenset)):
return frozenset(_make_hashable(v) for v in sorted(obj))
if isinstance(obj, Sequence):
# Treat bytes as already handled; for lists/tuples, recurse
return tuple(_make_hashable(v) for v in obj)
# Fallback: use object identity – safe for the duration of a single call
return id(obj)

def universal_lru_cache(maxsize=128, typed=False):
"""
LRU cache decorator that works with any arguments, mutable or not.
Mirrors functools.lru_cache signature.
"""
def decorator(user_func):
lock = threading.RLock()

@functools.wraps(user_func)
def wrapper(*args, **kwargs):
# Normalise args to a hashable key
key = (
tuple(_make_hashable(arg) for arg in args),
tuple(sorted((k, _make_hashable(v)) for k, v in kwargs.items()))
)
# Use the built‑in lru_cache on a thin inner function that receives the normalized key
@functools.lru_cache(maxsize=maxsize, typed=typed)
def _inner(normalised_key):
# Unpack the original args back – we don't need them here because
# the user function will be called with the *original* args/kwargs
return user_func(*args, **kwargs)

# The lock guards the first‑time creation of the inner function per wrapper
with lock:
return _inner(key)

# expose the original lru_cache helpers for introspection
wrapper.cache_info = lambda: wrapper.__wrapped__.cache_info()
wrapper.cache_clear = lambda: wrapper.__wrapped__.cache_clear()
return wrapper
return decorator


⚙️ Example usage

@universal_lru_cache(maxsize=4)
def expensive_sum(matrix):
"""Pretend this is a heavy computation on a mutable list‑of‑lists."""
total = 0
for row in matrix:
for val in row:
total += val
return total

# First call – cache miss
print(expensive_sum([[1, 2], [3, 4]])) # → 10

# Second call with an *identical* mutable structure – cache hit!
print(expensive_sum([[1, 2], [3, 4]])) # → 10 (instant)

# Different shape – cache miss
print(expensive_sum([[5, 5, 5], [5, 5, 5]])) # → 30

print(expensive_sum.cache_info())
# CacheInfo(hits=1, misses=2, maxsize=4, currsize=2)


🔎 Why this works

* The normalization step guarantees that two semantically equal containers produce the exact same hash.
By turning a list into a tuple (and recursively doing the same for nested structures), we preserve the element order while gaining immutability.

* Sorting mappings removes the nondeterminism of dictionary insertion order, so {a:1, b:2} and {b:2, a:1} collide on purpose – they represent the same logical input.

* Using id as a fallback ensures that objects without a natural immutable representation (e.g., file handles, custom classes without __hash__) still get a stable key for the duration of that call. It’s safe because the cache key lives only as long as the function call; the object’s identity won’t change.

(continued below 👇)
Wrapping an inner function with functools.lru_cache lets us reuse the battle‑tested LRU implementation (including thread‑locking, eviction policy, and statistics) without re‑implementing those low‑level details. The outer wrapper simply translates between real arguments and the canonical* hashable key.

🚀 Takeaway checklist

* ✅ You can now memoize functions that accept any mutable data structure.
* ✅ Cache hits are deterministic – identical logical inputs, regardless of mutability, map to the same entry.
* ✅ No performance penalty beyond the one‑time normalization (which is cheap for most data shapes).
* ✅ Still get cache_info() and cache_clear() for introspection and control.

Give it a spin in your next data‑heavy project and watch those redundant calculations vanish! 🌟
🚀 One‑Sitting Python Project: “Silent Clipboard Historian”

Ever copy‑and‑paste a piece of code, a link, or a snippet and later wish you could scroll back to the exact thing you copied ? A clipboard logger gives you a personal “undo” for the clipboard, works offline, and fits in a single script you can run whenever you need it.

Why it works

1. pyperclip talks to the OS clipboard in a cross‑platform way (Windows, macOS, Linux).
2. The script polls the clipboard every few hundred milliseconds – fast enough for human speed but light on CPU.
3. By keeping the last seen value in memory we can detect changes and avoid logging the same text over and over.
4. Each new entry is stamped with datetime.utcnow() and appended to a JSON file. JSON is human‑readable, easy to load later, and works without a DB server.
5. A tiny CLI helper (--show N) lets you peek at the last N entries without opening the file manually.

All of this fits into ≈ 60 lines and needs only two third‑party packages.

Libraries / concepts you’ll touch

- pyperclip – clipboard access.
- json – serialising a list of dictionaries.
- datetime – UTC timestamps, ISO‑8601 format.
- argparse – a friendly command‑line interface.
- Polling loop – while True: … time.sleep(0.5) – a simple event‑driven pattern without extra dependencies.

Quick sketch of the core loop

import json, time, argparse, sys
from datetime import datetime
import pyperclip

LOGFILE = "clipboard_history.json"

def load_log():
try:
with open(LOGFILE, "r", encoding="utf-8") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return []

def save_log(entry, history):
history.append(entry)
with open(LOGFILE, "w", encoding="utf-8") as f:
json.dump(history, f, ensure_ascii=False, indent=2)

def main(show_last: int = None):
if show_last:
# ----- quick read‑only mode -----
history = load_log()
for rec in history[-show_last:]:
print(f"[{rec['ts']}] {rec['text']!r}")
return

# ----- live‑logging mode -----
history = load_log()
last_clip = pyperclip.paste() # initialise with current content
print("📝 Clipboard historian started – press Ctrl‑C to stop.")
try:
while True:
cur = pyperclip.paste()
if cur != last_clip:
entry = {"ts": datetime.utcnow().isoformat() + "Z", "text": cur}
save_log(entry, history)
print(f"🔖 Captured at {entry['ts']}")
last_clip = cur
time.sleep(0.5) # half‑second pause → low CPU usage
except KeyboardInterrupt:
print("\n🛑 Stopped. History saved to", LOGFILE)

if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Silent Clipboard Historian – log everything you copy."
)
parser.add_argument(
"-n", "--show", type=int, metavar="N",
help="Print the last N clipboard entries and exit."
)
args = parser.parse_args()
main(show_last=args.show)


What the snippet does

- load_log() safely opens the JSON file; if it doesn’t exist we start with an empty list.
- save_log() appends a new dict ({"ts": "...", "text": "…"}) and writes the whole list back – JSON’s indent=2 keeps the file readable.
- In live‑logging mode we keep last_clip in a local variable; every loop we compare the freshly‑pasted text. If it changed, we create a timestamp with datetime.utcnow().isoformat() + "Z" (the Z marks UTC) and dump it.
- The --show N flag bypasses the loop, loads the file, and prints the most recent N lines – perfect for quick look‑ups.

How to run it

(continued below 👇)
pip install pyperclip          # one‑off dependency
python clipboard_hist.py # start logging
python clipboard_hist.py -n 5 # see the last 5 copies


Tips for extending (optional, after you’ve built it)

- Deduplication: store a hash of the text to avoid logging identical snippets that appear later.
- Desktop notifications: use plyer or win10toast to pop a tiny “saved” banner each time you copy.
- Search: add a --find <keyword> flag that filters entries containing the word.

Give it a try, tweak the polling interval if you feel adventurous, and enjoy having a personal “clipboard time‑machine” at your fingertips. Happy coding! 🎉
❤1
👋 Hey Python beginners!
Ever written a loop like this and wondered “How do I keep track of the position of each item?”

fruits = ["apple", "banana", "cherry"]
i = 0
for fruit in fruits:
print(i, fruit) # ❓ What’s i?
i += 1


You’ve just invented a counter yourself – that works, but Python already gives us a cleaner, safer tool: enumerate.

What enumerate does, in plain words
- enumerate takes any iterable (like a list, tuple, or string).
- It adds a running number (starting at 0 by default) to each element and hands you a pair (index, value).
- Think of it as a “smart counter” that never forgets to increase, even if you break out of the loop early.

Why it’s better than a manual counter
1. Less code, less chance of mistakes – you don’t need to create, update, and remember a separate variable.
2. Works automatically with break or continue. If you skip an iteration, the counter stays correct.
3. Readability: anyone reading your code immediately sees you’re dealing with both an item and its position.

Using enumerate – the basics

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

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


What happens?

1. enumerate(fruits) creates an iterator that yields (0, "apple"), then (1, "banana"), and so on.
2. The for loop unpacks each tuple into index and fruit.
3. Inside the body you can use both values freely.

Output

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


Custom start number

Sometimes you want counting to start at 1 (human‑friendly) instead of 0. Just pass a second argument:

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


Output

1. apple
2. banana
3. cherry


Why start=1 works: enumerate adds the offset you give it to the internal 0‑based counter before returning the pair.

Real‑world mini‑example: finding the first vowel in a word

word = "python"

for i, ch in enumerate(word):
if ch.lower() in "aeiou":
print(f"First vowel '{ch}' at position {i}")
break
else:
print("No vowel found")


- The loop checks each character.
- As soon as a vowel appears, we break out, and enumerate already knows the correct index (i).
- The else block runs only if the loop finishes without break, a handy pattern for “search‑and‑report” tasks.

TL;DR (the quick cheat‑sheet)

- enumerate(iterable, start=0) → gives you (index, value) pairs.
- Use it instead of manual counters for cleaner, safer loops.
- Adjust start when you need human‑readable numbering.

Give it a try in your next script – you’ll see how much smoother looping feels! 🚀

Happy coding! 🎉
🔧 Ever wanted to import Python code that lives somewhere other than a .py file on disk?

Maybe the source is stored in a database, fetched over the network, or generated on‑the‑fly.
The good news is that Python’s import machinery is entirely pluggable – you can teach the interpreter how to locate and load a module wherever you like.

Below is a self‑contained, intermediate‑to‑advanced example that shows you how to:

1. Create a custom finder that tells Python “I know how to locate this module”.
2. Build a loader that compiles the source code and injects the resulting module object.
3. Hook the finder into sys.meta_path so the interpreter automatically uses it.

> Why this works:
> The import system follows the PEP 302 protocol. When import foo runs, Python asks each entry in sys.meta_path (a list of finder objects) to produce a module spec (importlib.machinery.ModuleSpec). If a finder returns a spec, its associated loader is called to execute the module’s code. By providing our own finder/loader pair we can intercept the process at the exact point where the standard file‑system loader would normally kick in.

(continued below 👇)
# in_memory_importer.py
import sys
import types
import importlib.abc
import importlib.util
from hashlib import sha256

# ----------------------------------------------------------------------
# 1️⃣ A simple “virtual file system”: module name → source string
# ----------------------------------------------------------------------
VIRTUAL_MODULES = {
"demo_greeting": """
def hello(name: str) -> str:
return f"👋 Hello, {name}!"
""",
"demo_math": """
# A tiny math helper – deliberately kept in a string
def add(a, b):
return a + b

def mul(a, b):
return a * b
""",
}

# ----------------------------------------------------------------------
# 2️⃣ Finder – decides whether it can handle a given fully‑qualified name
# ----------------------------------------------------------------------
class InMemoryFinder(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path, target=None):
# `fullname` is the absolute module name (e.g. "demo_greeting")
if fullname in VIRTUAL_MODULES:
# We create a spec that points to our custom loader
return importlib.util.spec_from_loader(fullname, InMemoryLoader())
# Returning None tells Python to try the next finder in the chain
return None

# ----------------------------------------------------------------------
# 3️⃣ Loader – turns the source string into a real module object
# ----------------------------------------------------------------------
class InMemoryLoader(importlib.abc.Loader):
def create_module(self, spec):
"""
Optional hook. Returning None means “use the default module creation
semantics” (i.e. a plain `types.ModuleType`). Keeping it simple.
"""
return None

def exec_module(self, module):
"""
This is where the actual code execution happens.
`module.__spec__` already contains the name we need.
"""
source = VIRTUAL_MODULES[module.__spec__.name]
# Compile the source to a code object – this gives us a proper
# filename for tracebacks and enables `inspect.getsource`.
code = compile(source, f"<in‑memory:{module.__spec__.name}>", "exec")
# Execute the code in the module’s namespace.
exec(code, module.__dict__)

# Optional: give the module a stable __file__ attribute.
# Some libraries (e.g. pathlib.Path(__file__).parent) rely on it.
module.__file__ = f"<in‑memory:{module.__spec__.name}>"

# Optional: compute a deterministic hash for caching tools.
module.__hash__ = sha256(source.encode()).hexdigest()

# ----------------------------------------------------------------------
# 4️⃣ Register the finder – do this *once* at program start‑up
# ----------------------------------------------------------------------
sys.meta_path.insert(0, InMemoryFinder())

# ----------------------------------------------------------------------
# 5️⃣ Demo usage (you can delete this block in production)
# ----------------------------------------------------------------------
if __name__ == "__main__":
import demo_greeting
import demo_math

print(demo_greeting.hello("Ada"))
print("2 + 3 =", demo_math.add(2, 3))
print("4 * 5 =", demo_math.mul(4, 5))


How the pieces interact

(continued below 👇)