🚀 Shrink your objects, boost attribute look‑ups – unlock the power of
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
Why it works
* No per‑instance
* 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
Seeing the difference
Typical output on CPython 3.11 (64‑bit):
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 |
| Multiple inheritance with slots | Each base class must define its own
| Pickling |
| Dataclasses | Before Python 3.10 you needed a manual workaround. | From Python 3.10:
When to use
* 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
A quick recipe for modern code
(continued below 👇)
__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 |
|--------|-------------------|----------------|
|
|
|
|
|
|
> Tip: If you don’t have
Sketch of the approach
1. Load / create a JSON file (
2. Parse the command‑line arguments (
3. Mutate the in‑memory dict:
*
*
*
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 👇)
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
🎯 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:
That works, but it’s a bit clunky and easy to forget the
🛠️ Enter
Why it works:
Under the hood,
📚 How to use it
Output
Key points
-
-
- 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
Output:
🚀 When is
- 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:
- Returns: an iterator of
- Common use:
Give it a try!
Take any list you already have (numbers, strings, even a list of dictionaries) and rewrite the loop with
Happy coding, and remember: small helpers like
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
enumerateenumerate 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
The usual suspects – globals, thread‑locals, or even passing arguments around – either break the async model or make the code noisy.
Enter
📚 The why – what makes
1. Isolation at the coroutine level
A Context is a snapshot of all active
2. Transparent propagation through
When you
3. Explicit reset & token semantics
Setting a
4. Compatibility with
⚙️ The how – practical patterns
1️⃣ Basic usage – a request‑id logger
Why it works:
- Each
- The
- When we
2️⃣ Propagating context into a thread pool
Why it works:
3️⃣ Temporary overrides with
(continued below 👇)
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:
🛡️ Common pitfalls & how to avoid them
- Never store mutable objects directly in a
The Context only copies the reference; mutating the object will be seen by every task that holds the same reference. Use immutable types (
- Don’t forget the default!
If you access a
- Be wary of
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 (
🎯 TL;DR – when to reach for
- 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:
Happy coding! 🚀
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
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 (
- Regular expressions to extract URLs from raw text (
- Requests (or the lighter
- Set to deduplicate URLs before hitting the network.
- ThreadPoolExecutor (from
The “why” behind the choices
- Pathlib abstracts away Windows vs. POSIX path quirks, letting you write
- 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
- 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
A minimal skeleton to spark you
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
- Integrate with GitHub Actions to run automatically on PRs.
(continued below 👇)
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:
It works, but it’s a bit clunky and can lead to hidden bugs. Let’s unpack why
📚 What’s really happening?
-
- Every iteration the interpreter looks up
- If you ever change the list length inside the loop (e.g.,
✅ The tidy way with
What you see:
-
-
- No manual indexing, no extra
Why it works:
🎯 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
Mistake you might make — What goes wrong — How
Using
Forgetting to add
Manually managing two counters (e.g.,
🛠️ Quick tip: start counting at 1
If you want the first item to be numbered 1 instead of 0, just give
Output:
📌 TL;DR (Take‑away)
-
- It avoids manual index maths, reduces error‑prone code, and works with any iterable (lists, strings, tuples, even generators).
- Remember the optional
Give it a spin in your next script, and you’ll wonder how you ever lived without it! 🚀
Happy coding! 🎉
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
enumeratefruits = ["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 helpsUsing
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 iterableForgetting 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
But there’s a hidden snag:
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
* For custom objects we fall back to their
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
3. LRU mechanics – We reuse
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
💡 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 👇)
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
🚀 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
Give it a spin in your next data‑heavy project and watch those redundant calculations vanish! 🌟
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.
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
5. A tiny CLI helper (
All of this fits into ≈ 60 lines and needs only two third‑party packages.
Libraries / concepts you’ll touch
-
-
-
-
- Polling loop –
Quick sketch of the core loop
What the snippet does
-
-
- In live‑logging mode we keep
- The
How to run it
(continued below 👇)
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?”
You’ve just invented a counter yourself – that works, but Python already gives us a cleaner, safer tool:
What
-
- It adds a running number (starting at 0 by default) to each element and hands you a pair (
- 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
3. Readability: anyone reading your code immediately sees you’re dealing with both an item and its position.
Using
What happens?
1.
2. The
3. Inside the body you can use both values freely.
Output
Custom start number
Sometimes you want counting to start at 1 (human‑friendly) instead of 0. Just pass a second argument:
Output
Why
Real‑world mini‑example: finding the first vowel in a word
- The loop checks each character.
- As soon as a vowel appears, we
- The
TL;DR (the quick cheat‑sheet)
-
- Use it instead of manual counters for cleaner, safer loops.
- Adjust
Give it a try in your next script – you’ll see how much smoother looping feels! 🚀
Happy coding! 🎉
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 basicsfruits = ["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
> Why this works:
> The import system follows the PEP 302 protocol. When
(continued below 👇)
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 👇)
-
-
-
- Why we compile: compiling gives us a code object that the interpreter can cache, introspect, and display proper tracebacks. It also lets tools like
- Thread safety: the import system already holds an internal lock (
When to reach for this pattern
- Configuration‑driven plugins stored in a database or remote service.
- On‑the‑fly generated code (e.g., JIT‑like scenarios where you synthesize Python source at runtime).
- Testing: you can inject mock modules without touching the filesystem, keeping test suites fast and isolated.
- Packaging tricks: bundle tiny helper modules inside a single binary (e.g., when using PyInstaller) and load them from a compressed archive.
💡 Quick sanity check: After running the script, try
Feel free to adapt the
sys.meta_path is a list of finder objects consulted in order. By inserting our InMemoryFinder at position 0 we give it priority over the built‑in file‑system finder.-
find_spec receives the full module name. If the name exists in our VIRTUAL_MODULES dict we hand back a ModuleSpec that points to InMemoryLoader. Returning None is crucial; it tells the interpreter “I don’t know this one, try the next finder”.-
exec_module receives the module object that Python already created (or that create_module returned). We fetch the source, compile it (so the code object carries a sensible filename and line numbers), and exec it in the module’s dictionary. After exec finishes the module’s globals contain the functions, classes, etc., exactly as if the code had been loaded from a .py file.- Why we compile: compiling gives us a code object that the interpreter can cache, introspect, and display proper tracebacks. It also lets tools like
inspect.getsource work because we set module.__file__ to a recognizable pseudo‑filename.- Thread safety: the import system already holds an internal lock (
importlib._bootstrap._lock) while loading a module, so our loader does not need extra synchronization – the same module will never be executed twice concurrently.When to reach for this pattern
- Configuration‑driven plugins stored in a database or remote service.
- On‑the‑fly generated code (e.g., JIT‑like scenarios where you synthesize Python source at runtime).
- Testing: you can inject mock modules without touching the filesystem, keeping test suites fast and isolated.
- Packaging tricks: bundle tiny helper modules inside a single binary (e.g., when using PyInstaller) and load them from a compressed archive.
💡 Quick sanity check: After running the script, try
import demo_greeting; print(demo_greeting.__file__). You’ll see the pseudo‑filename <in‑memory:demo_greeting>, proving that the module thinks it lives somewhere virtual – yet it behaves exactly like any regular module.Feel free to adapt the
VIRTUAL_MODULES dict into a database query, an HTTP call, or any other data source. The import system is flexible enough that once you master the finder/loader contract, you can make Python import from anywhere. 🚀🚀 One‑Sitting Python Project: “Smart Downloads Organizer”
Ever stare at a chaotic Downloads folder and wish the files would just file themselves? Build a tiny daemon that watches the folder, inspects each new item, and instantly ships it to a sensible sub‑directory (e.g., Images, Docs, Archives). All in under an hour, no GUI required.
Why it works
1. File‑system events are push‑based – the
2. MIME types give intent – Python’s built‑in
3. Atomic moves keep things safe –
4. Configurable rules = extensibility – By storing a tiny JSON/YAML mapping (
What you’ll need
*
*
*
* (Optional)
High‑level flow
1. Load / create a mapping (
2. Start a
3. For each
a. Wait a tiny moment (
b. Ask
c. Derive the top‑level category (
d. Resolve the destination folder (fallback to Others).
e.
4. Keep the observer alive with a clean
Code snapshot
(continued below 👇)
Ever stare at a chaotic Downloads folder and wish the files would just file themselves? Build a tiny daemon that watches the folder, inspects each new item, and instantly ships it to a sensible sub‑directory (e.g., Images, Docs, Archives). All in under an hour, no GUI required.
Why it works
1. File‑system events are push‑based – the
watchdog library creates an Observer that runs in a background thread and calls you only when something actually changes. No need for a busy‑wait loop that burns CPU. 2. MIME types give intent – Python’s built‑in
mimetypes module maps a filename (or its contents via magic if you want extra accuracy) to a high‑level type like image/jpeg or application/pdf. Those top‑level strings (image, video, text, application) become the basis for your target folders. 3. Atomic moves keep things safe –
shutil.move works across filesystems and automatically falls back to a copy‑+‑delete when a simple rename isn’t possible, so the script stays robust even if the user redirects downloads to a network drive. 4. Configurable rules = extensibility – By storing a tiny JSON/YAML mapping (
.pdf → Documents, *.zip → Archives) you let the user tweak the behavior without touching code. The core loop stays unchanged.What you’ll need
*
watchdog – watches directories for create/modify/delete events. *
mimetypes (standard library) – guesses the file’s type from its name. *
shutil & pathlib – safe, cross‑platform path handling and moving. * (Optional)
python-magic – for content‑based MIME detection if you want extra accuracy. High‑level flow
1. Load / create a mapping (
type → folder). 2. Start a
watchdog.Observer on the target directory. 3. For each
FileCreatedEvent: a. Wait a tiny moment (
time.sleep(0.5)) to let the download finish. b. Ask
mimetypes.guess_type for the MIME string. c. Derive the top‑level category (
image, text, application, …). d. Resolve the destination folder (fallback to Others).
e.
shutil.move the file there, creating the folder if needed. 4. Keep the observer alive with a clean
try/except KeyboardInterrupt block.Code snapshot
(continued below 👇)
import json
import time
import shutil
from pathlib import Path
import mimetypes
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
# ---- Config ---------------------------------------------------------
# You can edit this file to change where each type lands.
CONFIG_PATH = Path.home() / ".downloads_organizer.json"
DEFAULT_MAP = {
"image": "Images",
"video": "Videos",
"text": "Documents",
"application": "Archives", # e.g. zip, pdf, exe
"audio": "Audio"
}
if CONFIG_PATH.exists():
TYPE_MAP = json.loads(CONFIG_PATH.read_text())
else:
TYPE_MAP = DEFAULT_MAP
CONFIG_PATH.write_text(json.dumps(DEFAULT_MAP, indent=2))
# ---- Event handler --------------------------------------------------
class DownloadHandler(FileSystemEventHandler):
def __init__(self, watch_path: Path):
self.watch_path = watch_path
def on_created(self, event):
# We only care about files, not sub‑folders.
if event.is_directory:
return
src = Path(event.src_path)
# Give the OS a moment to finish writing the file.
time.sleep(0.5)
# Guess MIME type → "image/jpeg" → "image"
mime, _ = mimetypes.guess_type(src.name)
category = mime.split("/")[0] if mime else "unknown"
# Resolve destination folder (fallback to "Others")
folder_name = TYPE_MAP.get(category, "Others")
dest_dir = self.watch_path / folder_name
dest_dir.mkdir(exist_ok=True)
dest = dest_dir / src.name
try:
shutil.move(str(src), str(dest))
print(f"🗂️ Moved {src.name} → {dest_dir.name}/")
except Exception as exc:
print(f"⚠️ Failed to move {src.name}: {exc}")
# ---- Main loop ------------------------------------------------------
def main():
downloads = Path.home() / "Downloads"
handler = DownloadHandler(downloads)
observer = Observer()
observer.schedule(handler, str(downloads), recursive=False)
observer.start()
print(f"👀 Watching {downloads} … Press Ctrl+C to stop.")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
if __name__ == "__main__":
main()
What’s happening under the hood?
Observer.schedule registers handler for the Downloads* directory. * When the OS creates a new file,
on_created fires once – no polling. *
mimetypes.guess_type simply looks at the file extension; it’s fast and requires no extra binaries. *
shutil.move abstracts away os.rename vs. copy‑+‑delete, so you don’t need to think about cross‑disk moves. ⚡️ Take it further (optional challenges)
* Replace
mimetypes with magic.from_file(..., mime=True) for content‑based detection. Add a dry‑run* flag that only prints actions.
* Hook into the system notification center (
plyer or notify2) to pop a toast each time a file lands. Persist a log* (
sqlite or plain CSV) of moved files for later analytics.Give it a spin, watch your Downloads tidy itself, and feel the instant gratification of a script that actually does something useful every time you grab a file. Happy hacking! 🎉