` or different class.
+
+**Lesson:** prefer a proper JSON API whenever one exists.
-6. Then we have printed the converted amount. We have used the `print()` function to print the converted amount.
-```python title="currencyconverter.py" showLineNumbers{1} {1}
-print(f'{amount} {from_currency} = {converted_amount}')
+## Part 2 — The API Version (Recommended)
+
+Use **exchangerate.host** — free, no key, JSON, stable interface.
+
+```python title="api_version.py"
+import requests
+
+API = "https://api.exchangerate.host/latest"
+
+def convert(amount: float, src: str, dst: str) -> float:
+ r = requests.get(API, params={"base": src, "symbols": dst}, timeout=10)
+ r.raise_for_status()
+ rate = r.json()["rates"][dst]
+ return amount * rate
+
+print(f"{convert(100, 'USD', 'INR'):.2f} INR")
```
+- One call, structured response, no HTML parsing.
+- `r.raise_for_status()` raises an exception on 4xx/5xx.
+- `r.json()` decodes the body into a Python dict.
-## Next Steps
-Congratulations! 🎉 You have successfully created a Currency Converter in Python.
+JSON looks like:
+```json title="response.json"
+{ "base": "USD", "rates": { "INR": 83.10 } }
+```
+
+The same site supports historical rates (`/2024-01-15`) and time series (`/timeseries?...`).
+
+## Add Caching
+
+Hitting the network every time is slow and wasteful — exchange rates do not change every second. Cache by source/target/date:
+
+```python title="cache.py"
+from functools import lru_cache
+from datetime import date
+
+@lru_cache(maxsize=128)
+def rate(src: str, dst: str, day: date) -> float:
+ r = requests.get(f"{API}/{day.isoformat()}",
+ params={"base": src, "symbols": dst}, timeout=10)
+ r.raise_for_status()
+ return r.json()["rates"][dst]
-There are many ways to improve this program. Here are some ideas:
-- Add more currencies.
-- Add a GUI to this program.
-- Add a database to this program.
-- Add a history feature to this program.
-- Add a feature to this program to convert the currency of a country to all the other currencies.
-- Add a feature to this program to convert the currency of all the countries to the currency of a country.
+def convert(amount, src, dst):
+ return amount * rate(src.upper(), dst.upper(), date.today())
+```
+`@lru_cache` memoizes results — same arguments → no second network call.
+
+For long-running scripts that survive restarts, write a JSON file:
+```python title="disk_cache.py"
+import json, time, pathlib
+CACHE = pathlib.Path("rates_cache.json")
+TTL = 60 * 60 # 1 hour
+def get_rate(src, dst):
+ data = json.loads(CACHE.read_text()) if CACHE.exists() else {}
+ key = f"{src}_{dst}"
+ if key in data and time.time() - data[key]["ts"] < TTL:
+ return data[key]["rate"]
+ rate = fetch_live(src, dst)
+ data[key] = {"rate": rate, "ts": time.time()}
+ CACHE.write_text(json.dumps(data))
+ return rate
+```
+
+## Robust Error Handling
+
+A real tool handles every failure path with a friendly message:
+```python title="errors.py"
+import requests
+try:
+ rate = fetch_rate("USD", "XYZ")
+except requests.ConnectionError:
+ print("No internet connection.")
+except requests.Timeout:
+ print("Request timed out — try again later.")
+except requests.HTTPError as e:
+ print(f"Server returned {e.response.status_code}.")
+except KeyError:
+ print("Unknown currency code.")
+```
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `AttributeError: NoneType` | Scraper selector outdated | Switch to JSON API |
+| `JSONDecodeError` | Body was HTML, not JSON | Confirm endpoint URL; print `r.text` to debug |
+| Same rate cached forever | No TTL | Add expiry to disk cache |
+| Hangs on slow network | No `timeout` | Pass `timeout=10` to every `requests.get` |
+| `100,000 USD` rejected | Comma in number | Strip non-digit characters before `float()` |
+
+## Variations to Try
+
+### 1. CLI mode with `argparse`
+```bash title="cli"
+python currencyconverter.py 100 --from USD --to INR
+```
+```python title="cli.py"
+import argparse
+p = argparse.ArgumentParser()
+p.add_argument("amount", type=float)
+p.add_argument("--from", dest="src", required=True)
+p.add_argument("--to", dest="dst", required=True)
+args = p.parse_args()
+print(convert(args.amount, args.src, args.dst))
+```
+
+### 2. Batch convert
+Convert one amount into many currencies at once:
+```python title="batch.py"
+TARGETS = ["EUR", "GBP", "INR", "JPY", "CAD"]
+r = requests.get(API, params={"base": "USD", "symbols": ",".join(TARGETS)}).json()
+for code, rate in r["rates"].items():
+ print(f"100 USD = {100 * rate:.2f} {code}")
+```
+
+### 3. GUI version
+See [Currency Exchange Rate Calculator GUI](./currencyexchnageratecalculator) for a Tkinter version with dropdowns.
+
+### 4. Historical chart
+Pull `/timeseries` data and plot with `matplotlib`:
+```python title="chart.py"
+import matplotlib.pyplot as plt
+data = requests.get("https://api.exchangerate.host/timeseries",
+ params={"start_date":"2025-01-01","end_date":"2025-01-31",
+ "base":"USD","symbols":"INR"}).json()["rates"]
+dates = sorted(data.keys())
+rates = [data[d]["INR"] for d in dates]
+plt.plot(dates, rates); plt.xticks(rotation=45); plt.tight_layout(); plt.show()
+```
+
+### 5. Crypto support
+Use **CoinGecko's** free API for BTC, ETH, etc.
+
+### 6. Web dashboard
+Wrap with Flask (see [Basic Web Server](./basicwebserver)). Form on a page; POST returns the conversion.
+
+### 7. Live ticker
+Print the rate every 60 seconds; highlight changes with color.
+
+### 8. Telegram bot
+Use `python-telegram-bot` so a chat command `100 USD INR` returns the result.
+
+## Real-World Applications
+- Travel apps that show prices in your home currency.
+- E-commerce checkout flows for international customers.
+- Finance dashboards that consolidate P&L in a base currency.
+- Cryptocurrency portfolio trackers.
+- Anything where users type money and need a quick second opinion.
+
+## Best Practices Demonstrated
+- **API over scraping** when a stable API exists.
+- **Always set a timeout** on network calls.
+- **Cache by key + TTL** to be a polite client.
+- **Catch network errors** specifically — `ConnectionError`, `Timeout`, `HTTPError` mean different things.
+- **Validate currency codes** before sending — fail fast on typos.
+
+## Educational Value
+- HTTP fundamentals and JSON parsing.
+- Web scraping pitfalls and when to avoid it.
+- Caching strategies and TTL design.
+- Building a multi-mode tool (interactive + CLI).
+- Defensive coding around the network.
+
+## Next Steps
+- Build the **API version** above and retire the scraper.
+- Add **disk caching** with TTL.
+- Wrap with **argparse** for scriptable use.
+- Plot **30-day history** with matplotlib.
+- Combine with [Currency Exchange Rate Calculator GUI](./currencyexchnageratecalculator) for a desktop frontend.
## Conclusion
-In this project, we have learned how to create a Currency Converter in Python. We have learned how to use the `requests` module and `BeautifulSoup` module. We have learned how to request the URL and parse the HTML. We have learned how to get the user input and convert it to uppercase. We have learned how to print the converted amount. We have learned how to create a Currency Converter in Python. To find more projects like this you can visit Python Central Hub.
\ No newline at end of file
+You started by scraping a website, hit the brittleness wall, and rebuilt the same feature against a proper JSON API with caching and clean error handling. The "API over scraping" instinct is one of the most valuable habits you can form. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/currencyconverter.py). Explore more on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/currencyexchnageratecalculator.mdx b/src/content/docs/projects/Beginners/currencyexchnageratecalculator.mdx
index 182d45d6..56423dec 100644
--- a/src/content/docs/projects/Beginners/currencyexchnageratecalculator.mdx
+++ b/src/content/docs/projects/Beginners/currencyexchnageratecalculator.mdx
@@ -1,6 +1,6 @@
---
title: Currency Exchange Rate Calculator GUI
-description: Build a GUI application for real-time currency conversion using live exchange rate APIs
+description: Build a Tkinter currency converter that fetches live exchange rates from a public API. Learn GUI widgets, HTTP requests, JSON parsing, error handling, and how to grow a simple converter into a personal finance tool.
sidebar:
order: 52
hero:
@@ -14,83 +14,258 @@ import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Create a professional currency exchange rate calculator with a graphical interface that fetches real-time exchange rates from an API and converts between multiple currencies. This project demonstrates GUI development, API integration, and financial data processing.
+A currency converter is the perfect "small but real" beginner project: it has a user interface, talks to a live API on the internet, parses structured data (JSON), and produces output the user actually cares about. In this tutorial you will build a desktop GUI in Tkinter that lets the user type an amount, pick a source currency and a target currency from dropdowns, and click **Convert** to see today's rate applied.
-## Prerequisites
+You will learn:
+- How to build a Tkinter window with labels, entries, dropdowns, and buttons.
+- How to call an HTTP API with `requests` and parse JSON responses.
+- How to handle network errors gracefully so the app never crashes mid-conversation.
+- How to organize widget-driven code with callbacks.
+- How to extend the converter into a tool you might actually use.
-- Python 3.6 or above
-- Text Editor or IDE
-- Basic understanding of Python syntax
-- Knowledge of Tkinter for GUI development
-- Familiarity with HTTP requests and APIs
-- Understanding of JSON data processing
+## Prerequisites
-## Getting Started
+- Python 3.6 or above.
+- A text editor or IDE.
+- An internet connection.
+- Comfort installing packages with `pip`.
+- Familiarity with functions and conditionals.
-### Create a new project
-1. Create a new project folder and name it `currencyExchangeCalculator`.
-2. Create a new file and name it `currencyexchnageratecalculator.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into your `currencyexchnageratecalculator.py` file.
+## Install Dependencies
-### Install required dependencies
-1. Install requests library for API calls:
-```bash
+```bash title="install"
pip install requests
```
+- **`tkinter`** ships with Python — no install needed.
+- **`requests`** is the de-facto library for HTTP in Python.
+
+## Pick an Exchange-Rate API
+
+There are several free public APIs you can use without an API key:
+
+| API | URL pattern | Notes |
+|---|---|---|
+| **exchangerate-api** | `https://open.er-api.com/v6/latest/USD` | Free tier, no key, daily updates. |
+| **frankfurter.app** | `https://api.frankfurter.app/latest?from=USD&to=EUR` | Open-source, ECB rates. |
+| **Fixer.io** | `https://api.exchangerate.host/latest?base=USD` | Larger currency list. |
+
+For this tutorial we will use **exchangerate.host** (no key required). The response is JSON like:
+```json
+{
+ "base": "USD",
+ "rates": { "EUR": 0.92, "GBP": 0.79, "INR": 83.10, ... }
+}
+```
+
+> 💡 Any API can change its terms. If yours stops working, swap the URL — the parsing code is similar.
+
+## Getting Started
+
+### Create the project
+1. Create a folder named `currency-exchange-calculator`.
+2. Inside it, create `currencyexchnageratecalculator.py`.
+3. Open the folder in your editor.
### Write the code
-1. Add the following code to your `currencyexchnageratecalculator.py` file.
-2. Save the file.
-3. Run the following command to run the application.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\currencyExchangeCalculator> python currencyexchnageratecalculator.py
-[Currency Exchange Calculator GUI Opens]
-- Enter amount to convert
-- Select base currency from dropdown
-- Select target currency from dropdown
-- Click "Convert" to get real-time exchange rate
-- Result displayed in popup message
-Example: 100 USD to EUR = 85.23 EUR
+
+### Run it
+```bash title="run"
+python currencyexchnageratecalculator.py
+```
+A window appears. Enter `100`, pick USD → EUR, click **Convert**, and a popup shows the result.
+
+## Step-by-Step Explanation
+
+### 1. Import what you need
+```python title="currencyexchnageratecalculator.py"
+import tkinter as tk
+from tkinter import ttk, messagebox
+import requests
+```
+- `tkinter` for the window, `ttk` for *themed* widgets (dropdowns look prettier than the legacy `OptionMenu`), and `messagebox` for the result popup.
+- `requests` for the HTTP call.
+
+### 2. Create the main window
+```python title="currencyexchnageratecalculator.py"
+win = tk.Tk()
+win.title("Currency Exchange Calculator")
+win.geometry("400x300")
+```
+`Tk()` is the root window. `geometry("WxH")` sets a fixed size.
+
+### 3. Build the widgets
+```python title="currencyexchnageratecalculator.py"
+tk.Label(win, text="Amount:").pack()
+amount_entry = tk.Entry(win)
+amount_entry.pack()
+
+CURRENCIES = ["USD", "EUR", "GBP", "INR", "JPY", "AUD", "CAD", "CNY"]
+tk.Label(win, text="From:").pack()
+from_currency = ttk.Combobox(win, values=CURRENCIES)
+from_currency.set("USD")
+from_currency.pack()
+
+tk.Label(win, text="To:").pack()
+to_currency = ttk.Combobox(win, values=CURRENCIES)
+to_currency.set("EUR")
+to_currency.pack()
+```
+- `Entry` is a one-line text input.
+- `Combobox` is a dropdown the user can also type into; `.set(value)` chooses an initial selection.
+- `.pack()` is the simplest of Tkinter's layout managers — it stacks widgets top-to-bottom.
+
+### 4. The conversion callback
+```python title="currencyexchnageratecalculator.py"
+def exchange():
+ try:
+ amount = float(amount_entry.get())
+ except ValueError:
+ messagebox.showerror("Invalid input", "Amount must be a number.")
+ return
+
+ src = from_currency.get()
+ dst = to_currency.get()
+
+ try:
+ url = f"https://api.exchangerate.host/latest?base={src}&symbols={dst}"
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+ rates = response.json()["rates"]
+ rate = rates[dst]
+ except requests.exceptions.RequestException as e:
+ messagebox.showerror("Network error", str(e))
+ return
+ except KeyError:
+ messagebox.showerror("Bad response", f"No rate for {dst} found.")
+ return
+
+ converted = amount * rate
+ messagebox.showinfo(
+ "Result",
+ f"{amount:.2f} {src} = {converted:.2f} {dst} (rate {rate:.4f})",
+ )
```
+Three layers of error handling:
+1. **`ValueError`** if the amount field is not a number.
+2. **`RequestException`** if the network is down, the server is unreachable, the request times out, or the API returns 4xx/5xx (because of `raise_for_status()`).
+3. **`KeyError`** if the API responds successfully but does not include the requested currency.
-3. **Use the Application**
- - Enter the amount to convert
- - Select source currency from dropdown
- - Select target currency from dropdown
- - Click "Convert" to get real-time exchange rate
+The `timeout=10` keeps the GUI responsive even if the API hangs.
+### 5. Wire the button and start the loop
+```python title="currencyexchnageratecalculator.py"
+tk.Button(win, text="Convert", command=exchange).pack(pady=10)
+win.mainloop()
```
+- `command=exchange` registers the callback.
+- `win.mainloop()` enters Tkinter's event loop, the program lives here until the window closes.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| GUI freezes after click | Long HTTP call running on the main thread | Use `threading.Thread` for the request, update the UI via `win.after` |
+| `requests.exceptions.ConnectionError` | No internet, or API down | Show a friendly message; consider caching the last rate |
+| `KeyError: 'EUR'` | Misspelled currency code in the dropdown | Validate codes against a known list |
+| Result shows `9.999999999999999` | Floating-point display | Format with `:.2f` |
+| Multiple Convert clicks stack popups | User clicks while one is showing | Use `messagebox.showinfo`, which is modal — but consider disabling the button until the call completes |
+
+## Variations to Try
+
+### 1. Real-time updates
+Use `win.after(60_000, refresh_rates)` to re-fetch every minute and show a live "last updated" label.
+
+### 2. Live result label instead of popup
+Replace the `messagebox` with a `Label` in the window that updates after each conversion. Better UX.
+
+### 3. Conversion history
+Append each result to a `Listbox`:
+```python title="history.py"
+history_box.insert("end", f"{amount} {src} → {converted:.2f} {dst}")
+```
+Bonus: write the history to `history.csv` on exit.
+
+### 4. Two-way swap button
+A "⇄" button that swaps the From and To selections.
+
+### 5. Charts
+Use **matplotlib** to plot a 30-day history. The API typically supports `/timeseries`:
+```python title="chart.py"
+url = "https://api.exchangerate.host/timeseries?start_date=2025-01-01&end_date=2025-01-31&base=USD&symbols=EUR"
+```
+
+### 6. Larger currency list
+Pull the full list from the API itself:
+```python title="full_list.py"
+all_currencies = requests.get("https://api.exchangerate.host/symbols").json()["symbols"]
+```
+
+### 7. Crypto support
+Use **CoinGecko's** free API (no key required) for BTC, ETH, etc.
+
+### 8. Calculator-style entry
+Replace the single Entry with a numeric keypad of buttons. Helpful for touch-screen kiosks.
+
+### 9. Convert to/from multiple currencies at once
+Instead of a single "To", show a table of values across ten major currencies.
+
+### 10. Offline mode
+Cache the last successful response to `last_rates.json`. If the network fails, fall back to the cache and label the result as "stale".
+
+## Concept Spotlight: HTTP Errors
+
+`requests` returns a `Response` object that *does not* raise on 4xx/5xx by default. Explicit checking is essential:
+
+```python title="errors.py"
+r = requests.get(url, timeout=5)
+print(r.status_code) # 200 = OK, 404 = not found, 500 = server error
+r.raise_for_status() # raises HTTPError for 4xx/5xx
+data = r.json() # raises JSONDecodeError if body isn't JSON
+```
+
+The error categories you should plan for:
+- **Connection errors** — DNS failure, no internet, server unreachable.
+- **Timeout errors** — server took too long.
+- **HTTP errors** — server replied but with an error status.
+- **Decode errors** — server replied with non-JSON content.
+
+`requests.exceptions.RequestException` is the *base* of all of these, so a single `except RequestException` catches everything network-related.
+
+## Best Practices Demonstrated
+
+- **Validate input before making a network call.** Cheap checks first, expensive operations second.
+- **Always set a timeout.** Default is no limit — a hanging server can freeze your GUI forever.
+- **Use `raise_for_status()`** to turn HTTP errors into exceptions you can `except`.
+- **Show errors in the UI, not the terminal.** End users do not read terminals.
+- **Format numbers explicitly.** `:.2f` for currency always.
+
+## Real-World Applications
+
+- Travel apps that show prices in the user's home currency.
+- E-commerce checkout flows handling multiple currencies.
+- Internal finance dashboards showing daily P&L in a base currency.
+- Crypto portfolio trackers.
+- Currency-aware invoice generators.
+
+## Educational Value
-## Explanation
-
-1. The `import tkinter as tk` statement imports the Tkinter library for creating the GUI.
-2. The `import requests` statement imports the requests library for making API calls.
-3. The `win = Tk()` creates the main window for the application.
-4. The `win.geometry("700x400")` sets the window size to 700x400 pixels.
-5. The `def exchange():` function handles the currency conversion logic.
-6. The `requests.get()` function fetches real-time exchange rates from the API.
-7. The `response.json()` parses the JSON response from the API.
-8. The `rates[converted_currency]` extracts the specific exchange rate.
-9. The `messagebox.showinfo()` displays the conversion result in a popup.
-10. The `ttk.Combobox()` creates dropdown menus for currency selection.
-11. The `Button()` widget creates the convert button with the exchange function.
-12. The currency list includes major world currencies for conversion.
+This project teaches:
+- **GUI fundamentals** — windows, widgets, layouts, callbacks, event loops.
+- **HTTP and JSON** — talking to the wider web from Python.
+- **Robust error handling** — three layers, friendly messages.
+- **Separation of concerns** — UI building vs. business logic vs. networking.
+- **Working with currency** — formatting, decimal precision.
## Next Steps
-Congratulations! You have successfully created a Currency Exchange Rate Calculator GUI in Python. Experiment with the code and see if you can modify the application. Here are a few suggestions:
-- Add more currencies to the selection list
-- Implement historical exchange rate charts
-- Add currency conversion history tracking
-- Create offline mode with cached rates
-- Implement currency rate alerts and notifications
-- Add calculator-style number input
-- Create currency trend analysis features
-- Add favorite currency pairs
-- Implement rate comparison between different providers
+- Replace `messagebox` popups with an inline result label that updates immediately.
+- Add a "favorites" panel for currency pairs the user converts often.
+- Plug into a **historical-rates** endpoint and draw a chart with matplotlib.
+- Wrap the conversion logic in a class for easier testing.
+- Port the GUI to **CustomTkinter** for a modern look.
+- Add a **command-line mode**: `python currencyexchnageratecalculator.py --amount 100 --from USD --to EUR`.
## Conclusion
-In this project, you learned how to create a Currency Exchange Rate Calculator GUI in Python using Tkinter and API integration. You also learned about working with real-time data, JSON processing, and creating professional GUI applications. You can find the source code on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/currencyexchnageratecalculator.py)
+You built a real desktop application that talks to a live web service — the same architecture every modern app uses, just smaller. You handled bad input, network failures, and missing data without crashing the GUI. The full source is on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/currencyexchnageratecalculator.py). Explore more GUI and API projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/dicerolling.mdx b/src/content/docs/projects/Beginners/dicerolling.mdx
index 9a18bf04..637bfe58 100644
--- a/src/content/docs/projects/Beginners/dicerolling.mdx
+++ b/src/content/docs/projects/Beginners/dicerolling.mdx
@@ -1,6 +1,6 @@
---
title: Dice Rolling Simulator
-description: A simple dice rolling simulator using Python. You can use this to play board games or just for fun. This is a command line program. It helps you to learn about random module in Python.
+description: Build a command-line dice roller in Python. Learn the random module, function design, while loops, input handling, and how to extend a tiny program into a full-featured tabletop helper.
sidebar:
order: 8
hero:
@@ -13,24 +13,45 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Dice Rolling Simulator is a simple Python program that rolls a dice and gives you a random number between 1 and 6. You can use this to play board games or just for fun. This is a command line program. It helps you to learn about random module in Python. In this article, we will learn how to create a dice rolling simulator using Python. It uses Random module to generate random numbers. Also, we will learn how to create a function in Python.
+A Dice Rolling Simulator is one of the most rewarding beginner projects because it does something *visibly random* — every run produces a different result. Under the hood it teaches you four core skills you will use forever: importing modules from the standard library, defining your own functions, looping until a condition changes, and parsing user input.
+
+In this project we build a dice roller that:
+- Rolls a virtual six-sided die and prints the result.
+- Lets the user roll again or quit at any time.
+- Is easy to extend to support multiple dice, dice of any number of sides (d4, d20, d100), and even Dungeons & Dragons style notation like `3d6+2`.
+
+By the end you will understand `random.randint`, function definitions with `def`, the `while True` loop, and how a tiny script grows into a real tool.
## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
+- Python 3.6 or above ([download](https://www.python.org/downloads/)).
+- A code editor or IDE (VS Code, PyCharm, Sublime, etc.).
+- Completed the [Hello World project](./helloworld) — or general comfort running a `.py` file.
+- No external libraries are needed; `random` ships with Python.
+
+## Concepts You Will Use
+
+Before writing code, here is what each piece does conceptually:
+
+- **`random` module** — Python's standard library tool for pseudo-random numbers. It can pick integers, floats, items from a list, shuffle a sequence, and more.
+- **`random.randint(a, b)`** — returns a random integer `N` such that `a ≤ N ≤ b`. Both endpoints are *inclusive*, which is exactly what you want for dice (1 and 6 are valid rolls).
+- **`def`** — defines a function. Functions let you give a *name* to a block of code so you can reuse it.
+- **`while True:`** — an infinite loop. You leave it with a `break` statement when the user wants to quit.
+- **`input(prompt)`** — pauses the program, prints a prompt, waits for the user to press Enter, returns whatever they typed as a string.
+- **`.lower()`** — converts a string to lowercase, so `"Y"` and `"y"` both behave the same way.
## Getting Started
-#### Create a Project
+
+### Create the project
1. Create a folder named `dice-rolling-simulator`.
-2. Open the folder in your code editor or IDE.
-3. Create a file named `dicerolling.py`.
-4. Copy the code given and paste it in the `dicerolling.py` file.
+2. Open it in your code editor.
+3. Inside, create a file called `dicerolling.py`.
-## Write the Code
-1. Copy and paste the following code in the `dicerolling.py` file.
+### Write the code
+Add the following to `dicerolling.py`:
-2. Save the file.
-3. Open the terminal and navigate to the project folder `dice-rolling-simulator`.
+
+Save the file. Open the integrated terminal and run:
+
```cmd title="command" showLineNumbers{1} {1}
C:\Users\username\PythonCentralHub\projects\beginners\dice-rolling-simulator> python dicerolling.py
You rolled 4
@@ -41,44 +62,151 @@ You rolled 5
Do you want to roll again? (y/n): n
Thanks for playing!
```
-4. You can see the output in the terminal.
-## Explanation
-1. First, we need to import the `random` module. We will use this module to generate random numbers.
+Run it several times. Notice the numbers change each run — that is the random module doing its job.
+
+## Step-by-Step Explanation
+
+### 1. Import the random module
```python title="dicerolling.py" showLineNumbers{1} {1}
import random
```
+`import` makes another module's code available in yours. After this line, anything inside the `random` module is reachable as `random.something`. The module itself lives somewhere in your Python installation; you do not need to know where.
-2. Next, we need to create a function to roll the dice. We will use the `randint()` function from the `random` module to generate a random number between 1 and 6.
+### 2. Define the roll function
```python title="dicerolling.py" showLineNumbers{1} {1-2}
def roll():
return random.randint(1, 6)
```
+- `def roll():` declares a new function named `roll` that takes no arguments.
+- The indented body is the function's code.
+- `return` hands a value back to whoever called the function. Here we return a fresh random integer between 1 and 6.
+- Calling `roll()` now feels like rolling a real die — you do not care *how* it gets a number, only that you get one.
-3. Now, we need to call the `roll()` function to roll the dice. We will use a `while` loop to roll the dice until the user wants to stop.
-```python title="dicerolling.py" showLineNumbers{1} {1-7}
+Why bother with a function for one line? Because:
+- It documents intent: reading `roll()` is clearer than reading `random.randint(1, 6)` everywhere.
+- It is easy to change: if you later want a 20-sided die, you change one line.
+
+### 3. Loop until the user stops
+```python title="dicerolling.py" showLineNumbers{1} {1-5}
while True:
print(f"You rolled {roll()}")
play_again = input("Do you want to roll again? (y/n): ")
if play_again.lower() != "y":
break
```
+- `while True:` starts a loop that never ends *on its own* — you exit it manually.
+- `print(f"You rolled {roll()}")` is an **f-string**. The `f` prefix lets you embed expressions inside `{ }`. `roll()` is called, the result is converted to text, and inserted into the message.
+- `input(...)` shows the prompt and waits.
+- `play_again.lower() != "y"` is the check: if the user did not type `y` (in any case), `break` immediately exits the loop.
-4. Finally, we need to print a message to the user.
-```python title="dicerolling.py" showLineNumbers{1} {1-9}
+### 4. Say goodbye
+```python title="dicerolling.py" showLineNumbers{1} {1}
print("Thanks for playing!")
```
+This line runs once, after the loop ends.
-## Next Steps
-Congratulations! You have successfully created a dice rolling simulator using Python.
+## Pseudo-Random, Not Truly Random
+
+`random.randint` does not produce truly random numbers — computers cannot. It produces **pseudo-random** numbers from a deterministic algorithm seeded by the current time. For dice this is more than fine. For cryptography, banking, or anything security-sensitive, use the `secrets` module instead.
+
+You can demonstrate the determinism yourself:
+```python title="seeded.py"
+import random
+random.seed(42)
+print(random.randint(1, 6)) # Always prints the same number for seed 42
+```
+Seeding is useful for *reproducible* test runs.
+
+## Common Mistakes
+
+| Problem | Fix |
+|---|---|
+| `NameError: name 'random' is not defined` | You forgot `import random` at the top. |
+| The program crashes after one roll | You used `if play_again == "y": break` — backwards logic. Use `!= "y"` to break. |
+| The program never quits no matter what you type | `"Y"` is not equal to `"y"`. Use `.lower()` to normalize. |
+| `random.randint(1, 6.0)` errors with `TypeError` | `randint` needs integers — use `random.uniform(1, 6)` for floats. |
-There are many ways to improve this program. Here are some ideas:
-- Add a feature to roll multiple dice at once.
-- Add a feature to roll a dice with more than 6 sides.
-- Add a feature to roll a dice with different colors.
-- Add a feature to roll a dice with different shapes.
-- Create a GUI version of this program.
-- Create a web version of this program.
+## Variations to Try
+
+### 1. Roll multiple dice at once
+```python title="multi_dice.py"
+def roll_dice(count):
+ return [random.randint(1, 6) for _ in range(count)]
+
+results = roll_dice(3)
+print(f"You rolled: {results} total: {sum(results)}")
+```
+A **list comprehension** generates `count` rolls in one line, and `sum()` totals them.
+
+### 2. Dice with any number of sides
+```python title="any_sides.py"
+def roll(sides=6):
+ return random.randint(1, sides)
+
+print(roll()) # six-sided
+print(roll(20)) # twenty-sided
+print(roll(100)) # percentile die
+```
+`sides=6` is a **default argument** — if the caller does not specify a value, Python uses 6.
+
+### 3. Parse D&D-style notation (`3d6+2`)
+```python title="dnd_notation.py"
+import re, random
+
+def parse_and_roll(notation):
+ match = re.fullmatch(r"(\d+)d(\d+)([+-]\d+)?", notation.replace(" ", ""))
+ if not match:
+ raise ValueError(f"Bad notation: {notation}")
+ count, sides, modifier = match.groups()
+ rolls = [random.randint(1, int(sides)) for _ in range(int(count))]
+ total = sum(rolls) + (int(modifier) if modifier else 0)
+ return rolls, total
+
+print(parse_and_roll("3d6+2"))
+# Example output: ([4, 2, 5], 13)
+```
+This shows how a simple project naturally grows into using **regular expressions** and **error handling**.
+
+### 4. ASCII-art dice faces
+```python title="ascii_dice.py"
+FACES = {
+ 1: ("┌─────────┐", "│ │", "│ ● │", "│ │", "└─────────┘"),
+ 2: ("┌─────────┐", "│ ● │", "│ │", "│ ● │", "└─────────┘"),
+ # …add 3, 4, 5, 6
+}
+
+for line in FACES[roll()]:
+ print(line)
+```
+A dictionary maps each face to its ASCII art.
+
+### 5. Statistics over many rolls
+```python title="stats.py"
+from collections import Counter
+rolls = [random.randint(1, 6) for _ in range(10_000)]
+print(Counter(rolls))
+```
+You should see roughly 1666 occurrences of each face — proof the distribution is uniform.
+
+## Real-World Applications
+- **Tabletop games** — replace lost dice, run probability experiments before a game session.
+- **Monte Carlo simulations** — repeated random sampling is the core of physics and finance simulations.
+- **Game development** — every roguelike, RPG, or loot system relies on randomness.
+- **Teaching probability** — let students *see* the law of large numbers by rolling 100,000 dice.
+
+## Features
+- Uses Python's standard library only — no installs.
+- Demonstrates functions, loops, conditionals, and input parsing in under 15 lines.
+- Easy to extend: any-sided dice, multiple dice, parsed notation, GUI.
+
+## Next Steps
+You now have a working dice simulator. Push yourself with one of these challenges:
+- Add an **average** that updates after each roll.
+- Add a **history** of the last 10 rolls.
+- Wrap it in a **Tkinter GUI** with a big "Roll!" button.
+- Build a **web version** with Flask that lets a remote player roll dice in a shared room.
+- Turn it into a **probability tool** that calculates the chance of beating a target with N dice of S sides.
## Conclusion
-In this article, we have learned how to create a dice rolling simulator using Python. We have used the `random` module to generate random numbers. Also, we have learned how to create a function in Python. To find more projects like this, you can check out the Python Central Hub.
\ No newline at end of file
+In this project you used Python's `random` module to simulate dice, learned to encapsulate behavior in a function, looped until the user wanted to stop, and saw several directions to extend the program. The dice roller is small but it touches every fundamental you need for bigger projects. Find more beginner projects on Python Central Hub and keep that momentum going.
diff --git a/src/content/docs/projects/Beginners/emailsender.mdx b/src/content/docs/projects/Beginners/emailsender.mdx
index 724c0744..deb64db5 100644
--- a/src/content/docs/projects/Beginners/emailsender.mdx
+++ b/src/content/docs/projects/Beginners/emailsender.mdx
@@ -1,6 +1,6 @@
---
title: Basic Email Sender
-description: A Python GUI application for sending emails using Gmail SMTP with Tkinter interface, featuring email validation and secure authentication.
+description: Build a Python email sender with Tkinter and smtplib. Learn SMTP, Gmail app passwords, HTML emails, attachments, multiple recipients, secrets management, and how to grow it into a real mailer.
sidebar:
order: 20
hero:
@@ -13,225 +13,267 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Basic Email Sender is a Python application with a graphical user interface that allows users to send emails through Gmail's SMTP server. The application features input validation, secure authentication using app passwords, and a clean Tkinter-based interface. This project demonstrates GUI development, email protocols, regular expressions for validation, and secure communication practices. It's an excellent project for learning about email automation and desktop application development.
+Email is older than the web and still drives most automated notifications, password resets, marketing flows, and alerts. In this project you will build a Tkinter GUI that sends real email via Gmail SMTP. We start with a plain-text version, then upgrade to HTML bodies, multiple recipients, file attachments, retries, secrets management, and end with a sketch of a transactional-email service. Along the way you learn why Gmail wants **app passwords** instead of your real one, and how `smtplib` lays out an email under the hood.
+
+You will learn:
+- The shape of SMTP — connect, STARTTLS, login, MAIL FROM, RCPT TO, DATA, QUIT.
+- Why Gmail blocks plain-password logins and how App Passwords fix it.
+- How `email.mime.*` builds multipart messages.
+- How to validate input safely and handle network errors.
+- How to manage secrets so credentials never sit in source control.
## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
-- tkinter module (usually comes pre-installed)
-- smtplib module (built-in)
-- Gmail account with 2-Step Verification enabled
+- Python 3.6 or above.
+- A text editor or IDE.
+- A Gmail account with **2-Step Verification** enabled.
+- An **App Password** generated at [myaccount.google.com/apppasswords](https://myaccount.google.com/apppasswords).
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download).
+## Set Up Gmail App Password
+1. Visit [myaccount.google.com/security](https://myaccount.google.com/security).
+2. Enable **2-Step Verification** (required to access App Passwords).
+3. Go to **App passwords** → generate a new one named "Python Mailer".
+4. Copy the 16-character code. This replaces your normal password in scripts.
+5. **Treat it like a password.** Anyone with this string can send mail as you.
-### Gmail App Password Setup
-To use Gmail with this application, you need to set up an App Password:
+## Getting Started
-1. Go to your Google Account settings
-2. Click on Security
-3. Under "Signing in to Google," select 2-Step Verification
-4. At the bottom of the page, select App passwords
-5. Enter a name that helps you remember where you'll use the app password
-6. Select Generate
-7. Copy the 16-character code that generates on your device
-8. Use this generated password in the application (not your regular Gmail password)
+### Create the project
+1. Create folder `email-sender`.
+2. Inside, create `emailsender.py`.
-For more information: [Google Support - App Passwords](https://support.google.com/mail/?p=InvalidSecondFactor)
+### Write the code
+
-## Getting Started
-#### Create a Project
-1. Create a folder named `email-sender`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `emailsender.py`.
-4. Copy the given code and paste it in your `emailsender.py` file.
-
-## Write the Code
-1. Copy and paste the following code in your `emailsender.py` file.
-
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `email-sender`.
-```cmd title="command" showLineNumbers{1} {1-5}
+### Run it
+```cmd title="command"
C:\Users\Your Name\email-sender> python emailsender.py
-# A GUI window will open with:
-# - Email input field
-# - Receiver's email field
-# - Password field
-# - Message field
-# - Send Email button
-# - Exit button
+# GUI opens with sender / receiver / password / message fields and Send button
+```
+
+## SMTP in Seven Lines
+
+The core is shorter than the GUI around it:
+```python title="core.py"
+import smtplib
+
+with smtplib.SMTP("smtp.gmail.com", 587) as server:
+ server.starttls()
+ server.login("you@gmail.com", "16-char-app-password")
+ server.sendmail("you@gmail.com", "friend@example.com",
+ "Subject: Hello\n\nThis is the body.")
```
+Step by step:
+1. **Connect** to Gmail's SMTP on port 587.
+2. **`STARTTLS`** upgrades the connection to encrypted.
+3. **Login** with your address + App Password.
+4. **`sendmail`** delivers the message.
+5. The `with` block automatically issues `QUIT` on exit.
+
+The `Subject:` header is required for Gmail to show a subject line; everything before the blank line is headers, everything after is the body.
-## Explanation
+## Step-by-Step (GUI Version)
-### Code Breakdown
-1. **Import required modules.**
-```python title="emailsender.py" showLineNumbers{1} {1-5}
-import tkinter as tk # pip install tk
-from tkinter import *
-import smtplib # pip install smtplib
+### 1. Imports
+```python title="emailsender.py"
+import tkinter as tk
from tkinter import messagebox
-import re
+import smtplib, re
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
```
-2. **Define the email sending function with validation.**
-```python title="emailsender.py" showLineNumbers{1} {1-10}
-def sendEmail():
+### 2. Build the form
+```python title="emailsender.py"
+root = tk.Tk()
+root.title("Email Sender")
+root.geometry("520x420")
+
+sender_entry = tk.Entry(root, width=50); sender_entry.pack(pady=5)
+receiver_entry = tk.Entry(root, width=50); receiver_entry.pack(pady=5)
+password_entry = tk.Entry(root, width=50, show="*"); password_entry.pack(pady=5)
+subject_entry = tk.Entry(root, width=50); subject_entry.pack(pady=5)
+message_text = tk.Text(root, width=50, height=10); message_text.pack(pady=5)
+```
+`show="*"` masks the password as the user types — a small but expected UX detail.
+
+### 3. Validate and send
+```python title="emailsender.py"
+EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
+
+def send():
+ sender = sender_entry.get().strip()
+ receivers = [r.strip() for r in receiver_entry.get().split(",") if r.strip()]
+ pwd = password_entry.get()
+ subject = subject_entry.get().strip() or "(no subject)"
+ body = message_text.get("1.0", "end").strip()
+
+ if not EMAIL_RE.match(sender):
+ return messagebox.showerror("Error", "Invalid sender address.")
+ for r in receivers:
+ if not EMAIL_RE.match(r):
+ return messagebox.showerror("Error", f"Invalid recipient: {r}")
+ if not pwd or not body:
+ return messagebox.showerror("Error", "Password and body required.")
+
+ msg = MIMEMultipart()
+ msg["From"] = sender
+ msg["To"] = ", ".join(receivers)
+ msg["Subject"] = subject
+ msg.attach(MIMEText(body, "plain"))
+
try:
- sender = email.get()
- rec = receiver.get()
- pas = password.get()
- msg = message.get()
-
- # Validating the Email
- if(sender == "Enter Your Email" or rec == "Enter Receiver's Email" or pas == "Enter Your Password" or msg == "Enter Your Message"):
- messagebox.showerror("Error", "Please Enter All The Fields")
- return
+ with smtplib.SMTP("smtp.gmail.com", 587) as server:
+ server.starttls()
+ server.login(sender, pwd)
+ server.send_message(msg, from_addr=sender, to_addrs=receivers)
+ except smtplib.SMTPAuthenticationError:
+ return messagebox.showerror("Auth failed", "Wrong password? Use an App Password.")
+ except smtplib.SMTPException as e:
+ return messagebox.showerror("SMTP error", str(e))
+ except Exception as e:
+ return messagebox.showerror("Network error", str(e))
+ messagebox.showinfo("Sent", "Email sent successfully.")
```
+`MIMEMultipart` is the foundation for attachments and HTML; even for plain text it costs almost nothing to use.
+
+## HTML Emails
-3. **Email validation using regular expressions.**
-```python title="emailsender.py" showLineNumbers{1} {1-8}
- # Validate Email Using Regular Expression
- email_regex = re.compile(r"[^@]+@[^@]+\.[^@]+")
- if not email_regex.match(sender):
- messagebox.showerror("Error", "Invalid Email")
- return
-
- if not email_regex.match(rec):
- messagebox.showerror("Error", "Invalid Receiver's Email")
- return
+To send rich content:
+```python title="html.py"
+html = """
Hello!
This is HTML email.
+
Click here
"""
+msg = MIMEMultipart("alternative")
+msg.attach(MIMEText("Plain-text fallback", "plain"))
+msg.attach(MIMEText(html, "html"))
```
+The `"alternative"` multipart type tells clients to show **one** of the parts — modern clients use HTML, plain-text-only clients fall back to the text part.
-4. **SMTP connection and email sending.**
-```python title="emailsender.py" showLineNumbers{1} {1-6}
- server = smtplib.SMTP('smtp.gmail.com', 587)
- server.starttls()
- server.login(sender, pas)
- server.sendmail(sender, rec, msg)
- server.quit()
- messagebox.showinfo("Success", "Email Sent Successfully")
+## Attachments
+
+```python title="attach.py"
+from email.mime.base import MIMEBase
+from email import encoders
+
+def attach_file(msg, path):
+ with open(path, "rb") as f:
+ part = MIMEBase("application", "octet-stream")
+ part.set_payload(f.read())
+ encoders.encode_base64(part)
+ part.add_header("Content-Disposition",
+ f'attachment; filename="{os.path.basename(path)}"')
+ msg.attach(part)
+
+attach_file(msg, "report.pdf")
+attach_file(msg, "screenshot.png")
```
+Files become base64-encoded MIME parts. Most providers cap attachments around 25 MB.
-5. **GUI setup and layout.**
-```python title="emailsender.py" showLineNumbers{1} {1-15}
-root = tk.Tk()
-root.title("Email Sender")
-root.geometry("700x700")
-root.config(background="white")
-
-label_file_explorer = Label(root, text = "Email Sender using Tkinter", width = 100, height = 3, fg = "gray", bg = "whitesmoke")
-
-email = Entry(root, width = 50)
-email.insert(0, "Enter Your Email")
-receiver = Entry(root, width = 50)
-receiver.insert(0, "Enter Receiver's Email")
-password = Entry(root, width = 50)
-password.insert(0, "Enter Your Password")
-message = Entry(root, width = 50)
-message.insert(0, "Enter Your Message")
+## Secret Management
+
+Hard-coding the App Password into `emailsender.py` is the most common security mistake in this project. Better:
+
+```python title="env.py"
+import os
+sender = os.environ["EMAIL_USER"]
+password = os.environ["EMAIL_PASSWORD"]
+```
+Set them once:
+```bash title="bash"
+export EMAIL_USER=you@gmail.com
+export EMAIL_PASSWORD=xxxx-xxxx-xxxx-xxxx
```
+Or use **`python-dotenv`** to read from a `.env` file (add `.env` to `.gitignore`).
-## Features
-- **Graphical User Interface:** Clean and intuitive Tkinter-based design
-- **Email Validation:** Regular expression-based email format checking
-- **Secure Authentication:** Uses Gmail's SMTP with TLS encryption
-- **Error Handling:** Comprehensive error messages and exception handling
-- **Input Validation:** Ensures all fields are filled before sending
-- **Success Feedback:** Confirms successful email transmission
-- **Field Reset:** Automatically clears and resets fields after sending
-
-## How to Use
-1. **Launch the Application:** Run the Python script to open the email sender window
-2. **Enter Sender Email:** Input your Gmail address
-3. **Enter Receiver Email:** Specify the recipient's email address
-4. **Enter App Password:** Use the generated Gmail app password (not your regular password)
-5. **Write Message:** Type your email message
-6. **Send Email:** Click the "Send Email" button
-7. **Check Status:** Success or error messages will appear in popup dialogs
-
-## Security Features
-- **TLS Encryption:** Uses secure TLS connection for email transmission
-- **App Password Support:** Works with Gmail's 2-Step Verification
-- **Input Validation:** Prevents injection attacks through validation
-- **Error Handling:** Secure error management without exposing sensitive data
-
-## SMTP Configuration
-The application uses Gmail's SMTP server configuration:
-- **Server:** smtp.gmail.com
-- **Port:** 587 (TLS)
-- **Authentication:** Required
-- **Encryption:** STARTTLS
-
-## Common Issues and Solutions
-
-### Authentication Error
-- **Problem:** "Authentication failed"
-- **Solution:** Use App Password instead of regular Gmail password
-- **Steps:** Enable 2-Step Verification and generate App Password
-
-### Connection Error
-- **Problem:** "Connection refused"
-- **Solution:** Check internet connection and firewall settings
-- **Alternative:** Try different network or VPN
-
-### Invalid Email Format
-- **Problem:** "Invalid email" error
-- **Solution:** Ensure email follows standard format (user@domain.com)
+For desktop apps, the OS keychain via **`keyring`** is the right answer:
+```python title="keychain.py"
+import keyring
+keyring.set_password("PCH-mailer", sender, app_password) # one-time setup
+pwd = keyring.get_password("PCH-mailer", sender) # later
+```
-## Next Steps
-You can enhance this project by:
-- Adding HTML email support for rich formatting
-- Implementing email templates and signatures
-- Adding file attachment functionality
-- Creating an email address book
-- Adding scheduling for delayed email sending
-- Implementing multiple email provider support (Yahoo, Outlook)
-- Adding email encryption and digital signatures
-- Creating batch email sending capabilities
-- Adding email tracking and read receipts
-- Implementing spam filtering and validation
-
-## Enhanced Version Ideas
-```python title="emailsender.py" showLineNumbers{1}
-def enhanced_email_sender():
- # Features to add:
- # - HTML email composer
- # - File attachment browser
- # - Email templates
- # - Address book integration
- # - Email scheduling
- # - Multiple recipient support
- pass
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| "Username and Password not accepted" | Used real Gmail password | Generate an App Password |
+| Mail goes to Spam | No proper `From`, no DKIM, plain-text body | Use SendGrid/Mailgun for production |
+| `ConnectionRefusedError` | Wrong port | 587 for STARTTLS, 465 for `smtplib.SMTP_SSL` |
+| Subject missing | Did not add the header | Set `msg["Subject"]` |
+| Emoji breaks subject | Default encoding mismatch | Use `MIMEText(body, _charset="utf-8")` |
+| Multiple recipients only first receives | Passed string, not list | Split on commas and pass a list |
+
+## Variations to Try
+
+### 1. CC and BCC
+```python title="cc_bcc.py"
+msg["Cc"] = "manager@example.com"
+all_recipients = receivers + ["manager@example.com", "hidden@example.com"]
+server.send_message(msg, from_addr=sender, to_addrs=all_recipients)
+```
+BCC is just a recipient with no header — pass to `send_message`'s `to_addrs` but don't add a header.
+
+### 2. Personalized mail merge
+```python title="merge.py"
+for row in csv.DictReader(open("contacts.csv")):
+ body = template.format(**row)
+ send(row["email"], body)
```
+Read CSV → format template per row → send.
-## Email Security Best Practices
-- **Use App Passwords:** Never use your main Gmail password
-- **Enable 2FA:** Always use two-factor authentication
-- **Validate Inputs:** Sanitize all user inputs
-- **Secure Storage:** Don't store passwords in plain text
-- **Rate Limiting:** Implement sending limits to prevent spam
+### 3. Other providers
+- **Outlook**: `smtp-mail.outlook.com:587`.
+- **Yahoo**: `smtp.mail.yahoo.com:587` (also app passwords).
+- **iCloud**: `smtp.mail.me.com:587`.
-## Educational Value
-This project teaches:
-- **GUI Development:** Creating desktop applications with Tkinter
-- **Email Protocols:** Understanding SMTP and email transmission
-- **Regular Expressions:** Pattern matching for validation
-- **Error Handling:** Managing exceptions and user feedback
-- **Security Practices:** Implementing secure authentication
+### 4. Inline images
+Use `MIMEImage` and reference with `

`. The `cid:` matches a `Content-ID` header on the image part.
+
+### 5. Scheduled send
+Combine with [Simple Reminder App](./simplereminderapp) — fire `send()` at a specific time.
+
+### 6. Bounce handling
+Read the SMTP response. `server.send_message` returns a dict of failed recipients.
+
+### 7. Production-grade with SendGrid / Mailgun
+For more than a few emails per day, switch to a transactional-email provider. They handle deliverability, SPF/DKIM signing, bounce processing, and analytics.
+
+### 8. CLI mode with `argparse`
+```bash title="cli"
+mailer.py --to friend@example.com --subject Hi --body "Hello!" --attach report.pdf
+```
+
+### 9. Email server preview with MailHog
+For development: run `mailhog` locally, point SMTP at `localhost:1025`, see every sent email in a web UI. Never accidentally email real users from a dev script.
+
+### 10. Sign and encrypt
+**S/MIME** or **PGP/MIME** for confidential payloads. Heavy but standardized.
+
+## Anti-Spam / Anti-Abuse Notes
+- Gmail caps your sending to ~500 messages/day for personal accounts.
+- Sending more than a handful per minute will get you throttled.
+- Marketing emails *must* include an unsubscribe link by law (CAN-SPAM, GDPR).
+- Always include a real `From` name and reply-to that goes somewhere real.
## Real-World Applications
-- **Automated Notifications:** Send system alerts and notifications
-- **Marketing Campaigns:** Automated email marketing tools
-- **Personal Automation:** Send regular updates or reminders
-- **Business Communication:** Internal communication tools
-- **Integration Scripts:** Add email functionality to other applications
-
-## Troubleshooting Tips
-- **Test with Known Emails:** Start with your own email addresses
-- **Check Spam Folders:** Sent emails might end up in spam
-- **Verify Credentials:** Double-check app password generation
-- **Network Issues:** Ensure stable internet connection
-- **Gmail Limits:** Be aware of Gmail's sending rate limits
+- **Account flows** — signup confirmations, password resets, 2FA codes.
+- **System alerts** — Cron job failed, disk filling up.
+- **Marketing** — newsletters, drip campaigns (use a provider for these).
+- **Reports** — daily summaries from a script.
+- **Internal tooling** — meeting reminders, expense approvals.
+
+## Educational Value
+- **SMTP protocol** — the wire format underlying every "send email" library.
+- **MIME** — multipart messages, content types, transfer encodings.
+- **Secrets management** — environment variables, keyring, dotenv.
+- **Validation** — regex for email syntax (note: regex *cannot* fully validate; SMTP verification is the only certain way).
+- **Provider quirks** — every SMTP server has its own gotchas; abstract the differences.
+
+## Next Steps
+- Move credentials into **environment variables** or **`keyring`**.
+- Add **attachments** and **HTML body**.
+- Wire up a **CSV-driven mail merge**.
+- Replace `smtplib` with **`yagmail`** for a cleaner Gmail-specific API.
+- Move to **SendGrid** or **Mailgun** for transactional use.
## Conclusion
-In this project, we learned how to create a Basic Email Sender using Python's Tkinter for the GUI and smtplib for email functionality. We explored secure email authentication, input validation using regular expressions, and error handling techniques. This project demonstrates the integration of multiple Python libraries to create a practical desktop application. The skills learned here are applicable to many automation and communication scenarios in software development. To find more projects like this, you can visit Python Central Hub.
+You built a real email-sending tool from scratch, learned why Gmail demands App Passwords, and saw how MIME multiparts model attachments and HTML bodies. The same patterns scale all the way up to corporate email pipelines — `smtplib` is the standard tool from beginner scripts to systems sending millions of messages a day. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/emailsender.py). Find more automation projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/fibonaccisequence.mdx b/src/content/docs/projects/Beginners/fibonaccisequence.mdx
index 88453771..dac4a80c 100644
--- a/src/content/docs/projects/Beginners/fibonaccisequence.mdx
+++ b/src/content/docs/projects/Beginners/fibonaccisequence.mdx
@@ -1,6 +1,6 @@
---
title: Fibonacci Sequence Generator
-description: A Python program that generates the Fibonacci sequence up to a specified number of terms, demonstrating mathematical sequences and iterative programming concepts.
+description: Generate the Fibonacci sequence in Python — iterative, recursive, memoized, generator, closed-form, and matrix-exponentiation versions. Learn algorithmic trade-offs and the golden ratio.
sidebar:
order: 45
hero:
@@ -13,33 +13,34 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Fibonacci Sequence Generator is a Python program that generates and displays the famous Fibonacci sequence up to a user-specified number of terms. The Fibonacci sequence is a mathematical series where each number is the sum of the two preceding numbers, starting with 0 and 1. This project demonstrates fundamental programming concepts including loops, variable management, user input handling, and mathematical computations. It's an excellent introduction to sequence generation and iterative algorithms.
+The Fibonacci sequence — `0, 1, 1, 2, 3, 5, 8, 13, 21, …` — is the most famous integer sequence in mathematics. Each number is the sum of the previous two. It is a perfect teaching tool: trivial to define, deeply connected to the golden ratio, and a textbook example for comparing algorithm strategies. In this project you will implement six different ways to generate it — iterative, recursive, memoized recursive, generator-based, closed-form using the golden ratio, and matrix exponentiation — and learn when each one is appropriate.
+
+You will leave understanding:
+- The definition: `F(0) = 0`, `F(1) = 1`, `F(n) = F(n-1) + F(n-2)`.
+- Why naïve recursion is exponentially slow.
+- How memoization turns it into linear time.
+- The golden-ratio closed form (and its precision limits).
+- Matrix exponentiation in `O(log n)` for huge `n`.
+- Why iterative is almost always the right answer in practice.
## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
-- Basic understanding of mathematical sequences
+- Python 3.6 or above.
+- A text editor or IDE.
+- Comfort with loops, recursion (helpful), and functions.
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download).
+## Getting Started
-Note: This project uses only built-in Python features, so no additional installations are required.
+### Create the project
+1. Create folder `fibonacci-generator`.
+2. Inside, create `fibonacci_sequence_generator.py`.
-## Getting Started
-#### Create a Project
-1. Create a folder named `fibonacci-generator`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `fibonacci_sequence_generator.py`.
-4. Copy the given code and paste it in your `fibonacci_sequence_generator.py` file.
-
-## Write the Code
-1. Copy and paste the following code in your `fibonacci_sequence_generator.py` file.
-
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `fibonacci-generator`.
-```cmd title="command" showLineNumbers{1} {1-20}
+### Write the code
+
+
+### Run it
+```cmd title="command"
C:\Users\Your Name\fibonacci-generator> python fibonacci_sequence_generator.py
-How many numbers that generates?: 10
+How many numbers? 10
Fibonacci sequence:
0
1
@@ -51,165 +52,240 @@ Fibonacci sequence:
13
21
34
+```
-C:\Users\Your Name\fibonacci-generator> python fibonacci_sequence_generator.py
-How many numbers that generates?: 1
-Fibonacci sequence upto 1 :
-0
+## Step-by-Step Explanation
+
+### 1. Iterative version (the default)
+```python title="iterative.py"
+def fib_iter(n: int) -> list[int]:
+ if n <= 0: return []
+ if n == 1: return [0]
+ out = [0, 1]
+ while len(out) < n:
+ out.append(out[-1] + out[-2])
+ return out
+```
+- O(n) time, O(n) space (storing the list).
+- Single allocation, no recursion overhead.
+- This is what 95 % of real-world Fibonacci code looks like.
+
+### 2. Bare minimum — just compute F(n)
+```python title="fib_n.py"
+def fib(n: int) -> int:
+ a, b = 0, 1
+ for _ in range(n):
+ a, b = b, a + b
+ return a
```
+- O(n) time, O(1) space.
+- The tuple assignment `a, b = b, a + b` is the Pythonic way to swap-and-update.
-## Explanation
+## Other Implementations
-### Understanding the Fibonacci Sequence
-The Fibonacci sequence is defined as:
-- F(0) = 0
-- F(1) = 1
-- F(n) = F(n-1) + F(n-2) for n > 1
+### 3. Naïve recursive (DO NOT use for large n)
+```python title="naive_rec.py"
+def fib_rec(n: int) -> int:
+ if n < 2: return n
+ return fib_rec(n - 1) + fib_rec(n - 2)
+```
+- O(2^n) time. `fib_rec(35)` takes seconds; `fib_rec(50)` takes minutes.
+- The same subproblems are recomputed billions of times.
+- Educational only — never ship this.
-This produces the sequence: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...
+### 4. Memoized recursive
+```python title="memo_rec.py"
+from functools import lru_cache
-### Code Breakdown
-1. **Get user input for the number of terms.**
-```python title="fibonacci_sequence_generator.py" showLineNumbers{1} {1}
-n = int(input("How many numbers that generates?: "))
+@lru_cache(maxsize=None)
+def fib_memo(n: int) -> int:
+ if n < 2: return n
+ return fib_memo(n - 1) + fib_memo(n - 2)
```
+- O(n) time, O(n) space.
+- One decorator turns exponential into linear.
+- Best example you will ever see of `@lru_cache`'s value.
-2. **Initialize the first two Fibonacci numbers and a counter.**
-```python title="fibonacci_sequence_generator.py" showLineNumbers{1} {1-3}
-n1 = 0
-n2 = 1
-count = 0
+### 5. Generator-based (memory-efficient)
+```python title="gen.py"
+def fib_gen():
+ a, b = 0, 1
+ while True:
+ yield a
+ a, b = b, a + b
+
+# usage
+import itertools
+print(list(itertools.islice(fib_gen(), 10)))
```
+- O(1) extra memory regardless of how far you go.
+- Lazy — computes only as many as you ask for.
+- Great when you want "the first N matching some condition" without precomputing.
+
+### 6. Closed form (Binet's formula)
+```python title="binet.py"
+import math
+PHI = (1 + math.sqrt(5)) / 2
+PSI = (1 - math.sqrt(5)) / 2
-3. **Handle edge cases for invalid input.**
-```python title="fibonacci_sequence_generator.py" showLineNumbers{1} {1-5}
-if n <= 0:
- print("Please enter a positive integer")
-elif n == 1:
- print("Fibonacci sequence upto", n, ":")
- print(n1)
+def fib_binet(n: int) -> int:
+ return round((PHI**n - PSI**n) / math.sqrt(5))
```
+- O(1) time (constant-time arithmetic).
+- Loses precision past `n ≈ 70` because of floating-point rounding.
+- Beautiful mathematically; not what you ship.
-4. **Generate and display the sequence using a while loop.**
-```python title="fibonacci_sequence_generator.py" showLineNumbers{1} {1-8}
-else:
- print("Fibonacci sequence:")
- while count < n:
- print(n1)
- nth = n1 + n2
- n1 = n2
- n2 = nth
- count += 1
+### 7. Matrix exponentiation (`O(log n)`)
+```python title="matrix.py"
+def fib_matrix(n: int) -> int:
+ def mat_mul(A, B):
+ return [
+ [A[0][0]*B[0][0] + A[0][1]*B[1][0], A[0][0]*B[0][1] + A[0][1]*B[1][1]],
+ [A[1][0]*B[0][0] + A[1][1]*B[1][0], A[1][0]*B[0][1] + A[1][1]*B[1][1]],
+ ]
+ def mat_pow(M, p):
+ result = [[1, 0], [0, 1]] # identity
+ while p:
+ if p & 1: result = mat_mul(result, M)
+ M = mat_mul(M, M)
+ p >>= 1
+ return result
+ if n == 0: return 0
+ return mat_pow([[1, 1], [1, 0]], n)[0][1]
```
+- O(log n) time using fast exponentiation.
+- Uses arbitrary-precision Python ints — no precision loss for huge n.
+- `fib_matrix(1_000_000)` computes in milliseconds.
+
+## Comparing the Approaches
-## Algorithm Explanation
+| Method | Time | Space | When to use |
+|---|---|---|---|
+| Iterative | O(n) | O(1) | Default choice |
+| Naïve recursion | O(2^n) | O(n) | Never (teaching only) |
+| Memoized recursion | O(n) | O(n) | Demonstrating DP / cache |
+| Generator | O(n) total | O(1) | Streaming or unknown count |
+| Binet's formula | O(1)* | O(1) | Tiny n, accept FP error |
+| Matrix exponentiation | O(log n) | O(1) | Massive n |
-### Step-by-Step Process
-1. **Initialization:** Set the first two numbers (0, 1) and a counter
-2. **Validation:** Check if the input is valid (positive integer)
-3. **Special Cases:** Handle n=1 separately
-4. **Loop Generation:** Use a while loop to calculate and display each term
-5. **Value Updates:** Update the two previous numbers for the next iteration
+*Constant-time only if you treat float ops as O(1). Above n ≈ 70 it returns wrong values.
+
+## Algorithm Walkthrough — Iterative for n = 5
-### Variable Tracking Example
-For n=5:
```
-Iteration 1: n1=0, n2=1, nth=1, print(0)
-Iteration 2: n1=1, n2=1, nth=2, print(1)
-Iteration 3: n1=1, n2=2, nth=3, print(1)
-Iteration 4: n1=2, n2=3, nth=5, print(2)
-Iteration 5: n1=3, n2=5, nth=8, print(3)
-Result: 0, 1, 1, 2, 3
+init a=0 b=1
+step 1 yield 0; a=1, b=1
+step 2 yield 1; a=1, b=2
+step 3 yield 1; a=2, b=3
+step 4 yield 2; a=3, b=5
+step 5 yield 3; a=5, b=8
+result: 0, 1, 1, 2, 3
```
-## Features
-- **User Input Validation:** Handles invalid and edge case inputs
-- **Flexible Output:** Generates any number of Fibonacci terms
-- **Clear Display:** Shows each term on a separate line
-- **Mathematical Accuracy:** Correctly implements the Fibonacci formula
-- **Memory Efficient:** Uses only three variables regardless of sequence length
-- **Educational Value:** Demonstrates iterative algorithm design
-
-## Mathematical Properties
-The Fibonacci sequence has many interesting properties:
-- **Golden Ratio:** The ratio of consecutive terms approaches φ ≈ 1.618
-- **Nature Patterns:** Appears in flower petals, pine cones, and spiral shells
-- **Mathematical Applications:** Used in computer algorithms and financial analysis
-
-## Algorithm Complexity
-- **Time Complexity:** O(n) - linear time
-- **Space Complexity:** O(1) - constant space
-- **Efficiency:** Very efficient for generating sequences
-
-## Common Use Cases
-- **Mathematical Education:** Teaching sequence generation
-- **Algorithm Practice:** Understanding iterative solutions
-- **Number Theory:** Exploring mathematical patterns
-- **Programming Interviews:** Common coding question
-- **Scientific Applications:** Modeling natural phenomena
+## Input Validation
-## Next Steps
-You can enhance this project by:
-- Adding a recursive implementation for comparison
-- Creating a GUI version using Tkinter
-- Implementing memoization for optimization
-- Adding visualization with graphs or charts
-- Creating different output formats (list, file, etc.)
-- Adding mathematical analysis (ratios, sums, etc.)
-- Implementing nth Fibonacci number calculation
-- Adding error handling for very large numbers
-- Creating performance benchmarks
-- Adding unit tests for validation
-
-## Alternative Implementations
-
-### Recursive Approach
-```python title="fibonacci_sequence_generator.py" showLineNumbers{1}
-def fibonacci_recursive(n):
- if n <= 1:
- return n
- else:
- return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)
+```python title="validate.py"
+def get_n():
+ while True:
+ raw = input("How many? ")
+ try:
+ n = int(raw)
+ if n < 0:
+ raise ValueError
+ return n
+ except ValueError:
+ print("Enter a non-negative integer.")
```
+Reject negative numbers and non-integers up front.
-### List-Based Approach
-```python title="fibonacci_sequence_generator.py" showLineNumbers{1}
-def fibonacci_list(n):
- fib_list = [0, 1]
- for i in range(2, n):
- fib_list.append(fib_list[i-1] + fib_list[i-2])
- return fib_list[:n]
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `RecursionError` | Naïve recursion past ~1000 | Use the iterative version |
+| Hang on n=40 | Naïve recursion's exponential blow-up | Switch to memoized or iterative |
+| Wrong result for n>70 | Floating-point precision in Binet | Use integer-based methods |
+| `IndexError` for n=0 or n=1 | Hard-coded `out[-2]` | Special-case small n |
+| Memory ballooning for huge n | Storing the full list | Use the generator if you only need streaming |
+
+## The Golden Ratio Connection
+
+The ratio of consecutive Fibonacci numbers approaches **φ ≈ 1.61803398875…**, the golden ratio:
+```python title="phi.py"
+for n in [5, 10, 20, 40, 80]:
+ print(n, fib(n+1) / fib(n))
+# 1.6, 1.617..., 1.6180..., 1.61803..., 1.61803398875...
```
+This is *why* Binet's formula works — `φ` is a root of `x² = x + 1`, the characteristic equation of the Fibonacci recurrence.
-### Generator Approach
-```python title="fibonacci_sequence_generator.py" showLineNumbers{1}
-def fibonacci_generator(n):
- a, b = 0, 1
- for _ in range(n):
- yield a
- a, b = b, a + b
+## Variations to Try
+
+### 1. Sum of Fibonacci numbers
+```python title="sum.py"
+def fib_sum(n): return sum(fib_iter(n))
+```
+Surprising identity: `F(0) + F(1) + … + F(n) = F(n+2) - 1`.
+
+### 2. Even-only Fibonacci (Project Euler #2)
+```python title="even.py"
+total = 0
+a, b = 0, 1
+while a < 4_000_000:
+ if a % 2 == 0: total += a
+ a, b = b, a + b
+```
+
+### 3. Plot the sequence
+```python title="plot.py"
+import matplotlib.pyplot as plt
+plt.plot(fib_iter(30), "o-"); plt.yscale("log"); plt.show()
```
+On a log scale, Fibonacci grows almost linearly — exponential growth made visual.
+
+### 4. Spiral drawing
+Use `turtle` to draw the famous Fibonacci spiral by chaining quarter-circles whose radii follow the sequence.
+
+### 5. nth Fibonacci modulo m (Pisano period)
+For very large n with a modulus, the sequence is periodic — useful in cryptography and contest programming.
+
+### 6. Tribonacci, Lucas, Pell sequences
+Same recurrence pattern, different starting values. Easy variations to implement once you have Fibonacci.
+
+### 7. Find n such that F(n) > 10^k
+Useful interview question — combine with a generator and a counter.
+
+### 8. Performance benchmark
+Time each method for n in `[10, 30, 100, 1000, 100_000]`. Plot results with matplotlib.
-## Performance Considerations
-- **Iterative vs Recursive:** Iterative is more efficient
-- **Memory Usage:** Current implementation uses minimal memory
-- **Large Numbers:** Python handles arbitrarily large integers
-- **Optimization:** Memoization can speed up recursive versions
+### 9. Decimal version
+For really, really huge n, Python's int already supports arbitrary precision. Try `len(str(fib(10_000_000)))` — you will get over 2 million digits.
+
+### 10. Web API
+Wrap with Flask: `GET /fib/
` returns JSON `{"n": n, "value": fib(n)}`. See [Basic Web Server](./basicwebserver).
+
+## Real-World Connections
+
+The sequence appears in:
+- **Nature** — sunflower seed spirals, pinecone scales, nautilus shells, flower petals.
+- **Markets** — Elliott Wave theory and Fibonacci retracement levels (anecdotal at best).
+- **Algorithms** — Fibonacci heaps (used in Dijkstra/Prim shortest-path).
+- **Number theory** — Zeckendorf's theorem: every positive integer is a unique sum of non-adjacent Fibonacci numbers.
+- **Computer science** — F-heaps, Fibonacci search, golden-ratio-based hashing.
## Educational Value
-This project teaches:
-- **Loop Control:** While loops and counters
-- **Variable Management:** Tracking multiple values
-- **Mathematical Sequences:** Understanding patterns
-- **Algorithm Design:** Iterative problem solving
-- **Input Validation:** Handling user input properly
-
-## Real-World Applications
-- **Computer Graphics:** Spiral patterns and fractals
-- **Financial Markets:** Elliott wave theory
-- **Architecture:** Golden ratio in design
-- **Biology:** Population growth models
-- **Algorithms:** Dynamic programming examples
+- **Algorithmic thinking** — six different solutions, six different trade-offs.
+- **Recursion vs. iteration** — both kinds of repetition compared head to head.
+- **Memoization (`@lru_cache`)** — one decorator, exponential speed-up.
+- **Generators** — laziness as a tool.
+- **Closed forms and their limits** — math vs. machine.
+- **Fast exponentiation** — divide-and-conquer over a sequence.
+
+## Next Steps
+- Run a **benchmark harness** comparing iterative, memoized, and matrix versions.
+- Implement **Tribonacci** and **Lucas** sequences using the same patterns.
+- Combine with [Reverse a String](./reversestring) to see another "many ways to solve" project.
+- Read about **Pisano periods** for modular Fibonacci.
+- Try **Project Euler problem #2** using your generator.
## Conclusion
-In this project, we learned how to generate the Fibonacci sequence using an iterative approach in Python. We explored fundamental programming concepts including loops, variable management, and mathematical computation. The Fibonacci sequence serves as an excellent introduction to algorithm design and demonstrates how simple rules can generate complex and beautiful patterns. This project provides a foundation for understanding more advanced mathematical and algorithmic concepts. To find more projects like this, you can visit Python Central Hub.
+Fibonacci is a one-line problem that pays off in spades the deeper you look. You implemented six solutions, learned why exponential recursion is a punishment, and saw how a single `@lru_cache` line rewrites the algorithm class. The same trade-offs recur in real systems — choosing iterative vs. recursive, caching vs. recomputing, fast paths vs. fallbacks. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/fibonacci_sequence_generator.py). Find more algorithm projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/fileexplorer.mdx b/src/content/docs/projects/Beginners/fileexplorer.mdx
index 146d197c..fc38afcc 100644
--- a/src/content/docs/projects/Beginners/fileexplorer.mdx
+++ b/src/content/docs/projects/Beginners/fileexplorer.mdx
@@ -1,6 +1,6 @@
---
title: Basic File Explorer
-description: A simple file explorer application built with Python's Tkinter library that allows users to browse, open, and read text files through a graphical interface.
+description: Build a Tkinter file browser in Python that opens, reads, and edits files. Learn file dialogs, text widgets, encoding handling, error recovery, syntax highlighting, and how to grow it into a real editor.
sidebar:
order: 17
hero:
@@ -13,161 +13,284 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Basic File Explorer is a graphical file browsing application built using Python's Tkinter library. The application provides a simple and intuitive interface for users to navigate their file system, select files, and view the contents of text files. It features a clean GUI with buttons for browsing files, displaying file paths, and showing file contents in a text area. This project is excellent for beginners to learn GUI development, file handling, and event-driven programming in Python.
+A file explorer is one of the most universally useful desktop GUIs you can build. In this project you will build a Python file viewer with Tkinter that lets the user pick any file via the OS file dialog, then displays its contents in a scrollable text area. Then we grow it: editable buffer, save-back, a directory tree, recent-files list, encoding detection, and a syntax-highlighted code view.
+
+You will learn:
+- How `filedialog` integrates with the native OS picker.
+- How to read text safely with the correct encoding.
+- How to wire scrollbars to a `Text` widget.
+- The `grid` vs `pack` decision for layout.
+- How to add real features (editing, saving, a directory tree) one at a time.
## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
-- tkinter module (usually comes pre-installed with Python)
+- Python 3.6 or above.
+- A text editor or IDE.
+- Tkinter (ships with most Python installs).
+- Comfort with classes (optional but recommended).
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download).
+On Linux you may need to install Tkinter separately:
+```bash title="install"
+sudo apt-get install python3-tk # Debian/Ubuntu
+sudo dnf install python3-tkinter # Fedora
+```
-Note: Tkinter usually comes pre-installed with Python. If you encounter import errors, you may need to install it separately:
+## Getting Started
-```cmd title="command" showLineNumbers{1} {1}
-# For Ubuntu/Debian
-sudo apt-get install python3-tk
+### Create the project
+1. Create folder `file-explorer`.
+2. Inside, create `fileexplorer.py`.
-# For CentOS/RHEL
-sudo yum install tkinter
-```
+### Write the code
+
-## Getting Started
-#### Create a Project
-1. Create a folder named `file-explorer`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `fileexplorer.py`.
-4. Copy the given code and paste it in your `fileexplorer.py` file.
-
-## Write the Code
-1. Copy and paste the following code in your `fileexplorer.py` file.
-
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `file-explorer`.
-```cmd title="command" showLineNumbers{1} {1-5}
+### Run it
+```cmd title="command"
C:\Users\Your Name\file-explorer> python fileexplorer.py
-# A GUI window will open with:
-# - "File Explorer using Tkinter" title
-# - "Browse Files" button
-# - "Exit" button
-# - Text area for displaying file contents
-# - Path display area
+# Window opens with:
+# - Title bar "File Explorer using Tkinter"
+# - "Browse Files" button
+# - "Exit" button
+# - Large text area
+# - "Path: ..." label at the bottom
```
+Click **Browse Files**, pick any text file, and its content appears in the panel.
-## Explanation
-1. Import the required modules.
-```python title="fileexplorer.py" showLineNumbers{1} {1-4}
-import tkinter as tk # pip install tk
-from tkinter import *
-from tkinter import filedialog, Text
-```
+## Step-by-Step Explanation
+
+### 1. Imports and root window
+```python title="fileexplorer.py"
+import tkinter as tk
+from tkinter import filedialog, Text, Label, Button, END
-2. Create the main window and initialize lists.
-```python title="fileexplorer.py" showLineNumbers{1} {1-3}
root = tk.Tk()
-apps = []
+root.title("File Explorer")
+root.geometry("700x700")
+root.config(background="white")
```
+- `Tk()` creates the main window.
+- `geometry("WxH")` sets size in pixels.
+- Importing names like `Text`, `Label`, `Button` directly is convenient — but only inside small scripts. For larger projects use `tk.Text`, `tk.Label`, etc.
-3. Define the file browsing function.
-```python title="fileexplorer.py" showLineNumbers{1} {1-10}
+### 2. The browse callback
+```python title="fileexplorer.py"
def browseFiles():
- output.delete('1.0', END)
- filename = filedialog.askopenfilename(initialdir = "/", title = "Select a File", filetypes = (("Text files","*.txt*"), ("all files", "*.*")))
- pathh.config(text = "Path: " + filename)
- tf = open(filename, encoding="utf8")
- data = tf.read()
+ output.delete("1.0", END)
+ filename = filedialog.askopenfilename(
+ initialdir="/",
+ title="Select a File",
+ filetypes=(("Text files", "*.txt"), ("All files", "*.*")),
+ )
+ if not filename:
+ return # user cancelled
+ path_label.config(text=f"Path: {filename}")
+ try:
+ with open(filename, encoding="utf-8") as f:
+ data = f.read()
+ except UnicodeDecodeError:
+ output.insert(END, "[binary or non-UTF-8 file]")
+ return
output.insert(END, data)
- tf.close()
```
+Three improvements over the naive version:
+- **Cancel-safe.** `askopenfilename` returns `""` when the user closes the dialog; we bail early.
+- **`with open(...)`** closes the file even on exception.
+- **`UnicodeDecodeError` handler.** Lets the program survive binary or weird-encoding files.
-4. Configure the main window.
-```python title="fileexplorer.py" showLineNumbers{1} {1-3}
-root.title("File Explorer")
-root.geometry("700x700")
-root.config(background="white")
+### 3. The widgets
+```python title="fileexplorer.py"
+title_label = Label(root, text="File Explorer using Tkinter",
+ width=100, height=3, fg="gray", bg="whitesmoke")
+browse_button = Button(root, text="Browse Files", command=browseFiles)
+exit_button = Button(root, text="Exit", command=root.destroy)
+output = Text(root, wrap="none")
+path_label = Label(root, text="Path: ", width=100, height=3, fg="gray", bg="whitesmoke")
```
+- `Text(root)` is a multi-line text widget; the line/character coordinates work like `"line.char"` (`"1.0"` means line 1, char 0).
+- `wrap="none"` shows long lines with horizontal scrolling rather than wrapping.
-5. Create GUI components.
-```python title="fileexplorer.py" showLineNumbers{1} {1-8}
-label_file_explorer = Label(root, text = "File Explorer using Tkinter", width = 100, height = 3, fg = "gray", bg = "whitesmoke")
+### 4. Layout with grid
+```python title="fileexplorer.py"
+title_label.grid(column=1, row=1)
+browse_button.grid(column=1, row=2)
+exit_button.grid(column=1, row=3)
+output.grid(column=1, row=4)
+path_label.grid(column=1, row=5)
-button_explore = Button(root, text = "Browse Files", command = browseFiles)
-exit_button = Button(root, text = "Exit", command = root.destroy)
-output = Text(root)
-pathh = Label(root, text = "Path: ", width = 100, height = 3, fg = "gray", bg = "whitesmoke")
+root.mainloop()
```
+`grid` arranges widgets in cells. Even with one column this gives a tidy vertical stack.
-6. Arrange components using grid layout.
-```python title="fileexplorer.py" showLineNumbers{1} {1-8}
-label_file_explorer.grid(column = 1, row = 1)
-button_explore.grid(column = 1, row = 2)
-exit_button.grid(column = 1, row = 3)
-output.grid(column = 1, row = 4)
-pathh.grid(column = 1, row = 5)
+## Add Scrollbars
-root.mainloop()
+A `Text` widget without scrollbars is unusable for big files. Wire one up:
+```python title="scroll.py"
+vsb = tk.Scrollbar(root, orient="vertical", command=output.yview)
+hsb = tk.Scrollbar(root, orient="horizontal", command=output.xview)
+output.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
+vsb.grid(column=2, row=4, sticky="ns")
+hsb.grid(column=1, row=6, sticky="ew")
```
+The bidirectional binding — `command=output.yview` plus `yscrollcommand=vsb.set` — connects scrollbar and text so they stay in sync.
-## Features
-- **Graphical User Interface:** Clean and intuitive Tkinter-based GUI
-- **File Browsing:** Navigate through the file system using file dialog
-- **File Type Filtering:** Filter files by type (text files, all files)
-- **File Content Display:** View the contents of selected text files
-- **Path Display:** Show the full path of the selected file
-- **UTF-8 Support:** Handle files with various character encodings
-- **Exit Functionality:** Clean application termination
-
-## How to Use
-1. **Launch the Application:** Run the Python script to open the file explorer window
-2. **Browse Files:** Click the "Browse Files" button to open the file selection dialog
-3. **Select a File:** Navigate through directories and select a file to open
-4. **View Contents:** The file's content will be displayed in the text area
-5. **Check Path:** The full file path will be shown at the bottom
-6. **Exit:** Click the "Exit" button to close the application
-
-## GUI Components Breakdown
-- **Title Label:** Displays the application name
-- **Browse Button:** Opens the file selection dialog
-- **Exit Button:** Closes the application
-- **Text Area:** Shows the content of selected files
-- **Path Label:** Displays the full path of the selected file
-
-## File Type Support
-The current implementation supports:
-- **Text Files (.txt):** Primary file type with dedicated filter
-- **All Files (*.*):** Fallback option for any file type
+## Make It Editable + Save
-## Next Steps
-You can enhance this project by:
-- Adding support for more file types (PDF, Word, images)
-- Implementing file editing capabilities
-- Adding a file tree view for better navigation
-- Creating file operations (copy, move, delete, rename)
-- Adding syntax highlighting for code files
-- Implementing search functionality within files
-- Adding recent files history
-- Creating bookmarks for frequently accessed folders
-- Adding file properties display (size, date modified, etc.)
-- Implementing drag and drop functionality
-
-## Error Handling Improvements
-Consider adding these error handling features:
-```python title="fileexplorer.py" showLineNumbers{1}
-def browseFiles():
+Tkinter's `Text` widget is already editable by default. To save changes back:
+```python title="save.py"
+def save_file():
+ if not current_path:
+ save_file_as()
+ return
try:
- output.delete('1.0', END)
- filename = filedialog.askopenfilename(...)
- if filename: # Check if file was selected
- pathh.config(text = "Path: " + filename)
- with open(filename, encoding="utf8") as tf:
- data = tf.read()
- output.insert(END, data)
- except UnicodeDecodeError:
- output.insert(END, "Error: Cannot decode file. File may be binary or use different encoding.")
- except Exception as e:
- output.insert(END, f"Error opening file: {str(e)}")
+ with open(current_path, "w", encoding="utf-8") as f:
+ f.write(output.get("1.0", END))
+ except OSError as e:
+ path_label.config(text=f"Save failed: {e}")
+
+def save_file_as():
+ path = filedialog.asksaveasfilename(defaultextension=".txt")
+ if path:
+ global current_path
+ current_path = path
+ save_file()
```
+- `output.get("1.0", END)` returns the entire text content.
+- `defaultextension=".txt"` ensures users get a sensible extension if they forget.
+
+## Encoding Detection
+
+Real files come in many encodings. **`chardet`** can guess:
+```bash title="install"
+pip install chardet
+```
+```python title="detect.py"
+import chardet
+with open(filename, "rb") as f:
+ raw = f.read()
+enc = chardet.detect(raw)["encoding"] or "utf-8"
+data = raw.decode(enc, errors="replace")
+```
+`errors="replace"` substitutes a replacement character for un-decodable bytes instead of crashing.
+
+## Add a Directory Tree
+
+For a "real" explorer, show a tree on the left:
+```python title="tree.py"
+from tkinter import ttk
+import os
+
+tree = ttk.Treeview(root)
+tree.grid(column=0, row=4, sticky="ns")
+
+def populate(parent, path):
+ for entry in sorted(os.listdir(path)):
+ full = os.path.join(path, entry)
+ node = tree.insert(parent, "end", text=entry, values=[full])
+ if os.path.isdir(full):
+ tree.insert(node, "end") # placeholder for expansion
+
+def on_open(event):
+ node = tree.focus()
+ tree.delete(*tree.get_children(node))
+ populate(node, tree.item(node, "values")[0])
+
+tree.bind("<>", on_open)
+populate("", "/")
+```
+Lazy expansion (insert one placeholder child, populate only when opened) keeps the UI snappy for huge directories.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| App crashes on cancel | `filename` is `""` | `if not filename: return` |
+| `UnicodeDecodeError` on binary file | Default encoding | Use `try/except UnicodeDecodeError` |
+| Old content still shown | Forgot to clear the widget | `output.delete("1.0", END)` first |
+| Scrollbar does not move | Did not wire `yscrollcommand` | Bidirectional binding both ways |
+| Large file freezes UI | Reading 1 GB into a `Text` widget | Stream / paginate, or refuse over a threshold |
+
+## Variations to Try
+
+### 1. Recent files
+Store the last 10 opened paths in a JSON file; show as a dropdown.
+
+### 2. Syntax highlighting
+Use **Pygments** to color Python/JS/HTML code:
+```python title="pygments.py"
+from pygments import lex
+from pygments.lexers import get_lexer_for_filename
+for token, value in lex(data, get_lexer_for_filename(filename)):
+ output.insert(END, value, str(token))
+```
+Define tag colors with `output.tag_configure("Token.Keyword", foreground="blue")`.
+
+### 3. Find / Replace
+A small dialog with two `Entry` widgets and Tkinter's built-in `Text` search:
+```python title="find.py"
+idx = output.search(term, "1.0", stopindex=END)
+if idx:
+ output.tag_add("sel", idx, f"{idx}+{len(term)}c")
+ output.see(idx)
+```
+
+### 4. Multiple tabs
+Use `ttk.Notebook` so each opened file lives in its own tab.
+
+### 5. Image preview
+For `.png/.jpg` files, swap the `Text` widget for a `Label` showing a `PIL.ImageTk.PhotoImage`.
+
+### 6. CSV viewer
+When opening a `.csv`, parse and display in a `ttk.Treeview` table instead of plain text.
+
+### 7. Markdown preview
+On open, render Markdown to HTML with `pip install markdown` and show in a side panel (use `tkhtmlview` for an HTML widget).
+
+### 8. Drag & drop
+With `tkinterdnd2`, dropping a file onto the window opens it.
+
+### 9. Recent edits tracker
+Compare on-open content with on-save content and show diff stats (lines added/removed).
+
+### 10. File operations
+Right-click context menu: Copy, Move, Rename, Delete — wire to `shutil.move`, `os.remove`, etc. Confirm before destructive operations.
+
+## Architecture: Move to a Class
+
+When the script grows past 100 lines, refactor:
+```python title="oop.py"
+class FileExplorer:
+ def __init__(self, root):
+ self.root = root
+ self.current_path = None
+ self._build_ui()
+
+ def _build_ui(self): ...
+ def browse(self): ...
+ def save(self): ...
+```
+Everything else stays the same — but state lives on `self`, not module globals, and you can test the class without launching a window.
+
+## Real-World Applications
+- **In-product file viewers** — log viewers, debug consoles.
+- **Internal tools** — quick text viewers for ops teams.
+- **Educational software** — viewing student submissions.
+- **Configuration editors** — load YAML/INI, validate on save.
+- **Prototype IDEs** — every editor started as a file viewer.
+
+## Educational Value
+- **Tkinter widgets** — buttons, labels, text, frames, scrollbars.
+- **OS file dialogs** — leveraging the native UI.
+- **Encoding handling** — what is at stake when the bytes are not ASCII.
+- **Event-driven design** — callbacks rather than top-down scripts.
+- **Layout reasoning** — `grid` vs `pack`, `sticky`, weights.
+
+## Next Steps
+- Add **scrollbars** (most important quick win).
+- Make the buffer **editable + saveable**.
+- Add **encoding detection** with `chardet`.
+- Add a **directory tree** with `ttk.Treeview`.
+- Layer **syntax highlighting** with Pygments.
+- Refactor into a class.
+- See [Basic Text Editor](./basic-text-editor) for the next evolution — a full editor with undo/redo, find/replace, and a menu bar.
## Conclusion
-In this project, we learned how to create a Basic File Explorer using Python's Tkinter library. We explored GUI development concepts including event handling, layout management, file dialogs, and text display widgets. This project demonstrates essential skills for desktop application development and provides a foundation for more complex file management applications. The combination of file handling and GUI programming makes this an excellent learning project for intermediate Python developers. To find more projects like this, you can visit Python Central Hub.
+You built a working file viewer, fixed three real-world bugs (cancel, encoding, missing scrollbars), and have a road map for turning it into a real editor. Tkinter is unfashionable but its model — widgets, callbacks, the event loop — matches every other GUI framework you will meet. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/fileexplorer.py). Explore more GUI projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/guessthenumber.mdx b/src/content/docs/projects/Beginners/guessthenumber.mdx
index 1c888599..27d76dbf 100644
--- a/src/content/docs/projects/Beginners/guessthenumber.mdx
+++ b/src/content/docs/projects/Beginners/guessthenumber.mdx
@@ -1,6 +1,6 @@
---
title: Guess the Number Game
-description: A simple game where the user has to guess a number between 1 and 20. This is a good example of using a while loop with a break statement.
+description: Build the classic "Guess the Number" game in Python. Master loops, conditionals, randomness, input validation, and learn how to extend it into a smarter AI-opponent version.
sidebar:
order: 3
hero:
@@ -13,27 +13,44 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-This is a simple game where the user has to guess a number between 1 and 20. You will learn how to use a while loop with a break statement. In this application, computer will choose a random number between 1 and 20, and the user will have to guess the number. If the user guesses wrong, the program will tell the user if the guess is too high or too low. If the user guesses correctly, the program will congratulate the user and end the game. If the user fails to guess the number within six tries, the program will tell the user the number and end the game.
+"Guess the Number" is the rite-of-passage game in every beginner curriculum, and for good reason: in a single short script you exercise nearly every fundamental — randomness, looping, branching, user input, type conversion, comparisons, and string formatting. The premise: the computer secretly chooses a number between 1 and 20, the user has six chances to guess it, and after every guess the program tells them whether they were too high or too low.
+
+In this tutorial we will:
+- Build the classic version from scratch.
+- Walk line-by-line through what each statement does.
+- Harden the program against bad input (letters, negative numbers, empty input).
+- Extend it with difficulty levels, score tracking, and a "play again" loop.
## Prerequisites
-- Python 3.6 or above
-- Text Editor or IDE
+- Python 3.6 or above.
+- A text editor or IDE.
+- Familiarity with running a Python script (see [Hello World](./helloworld)).
+- Some comfort with `input()`, `print()`, and basic comparisons.
+
+## Concepts You Will Use
+
+- **`random.randint(a, b)`** — picks a random integer between `a` and `b`, inclusive.
+- **`for` loop with `range`** — iterates a fixed number of times.
+- **`if`/`elif`/`else`** — choose between several branches.
+- **`break`** — exit a loop immediately.
+- **Type conversion** — `int(input(...))` turns text into a number.
+- **String concatenation vs. f-strings** — two ways to insert values into messages.
## Getting Started
-### Create a new project
-1. Create a new project folder and name it `guessTheNumberGame`.
-2. Create a new file and name it `guessTheNumberGame.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into your `guessTheNumberGame.py` file.
+
+### Create the project
+1. Make a folder named `guess-the-number`.
+2. Inside it, create a file named `guessthenumber.py`.
+3. Open the folder in your editor.
### Write the code
-1. Add the following code to your `guessTheNumberGame.py` file.
+Paste the following into `guessthenumber.py`:
-2. Save the file.
-3. Open the terminal and navigate to the project folder.
-4. Run the following command to run the application.
+
+Save, open a terminal in the folder, and run:
+
```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\guessTheNumberGame> python guessthenumber.py
+C:\Users\username\Documents\guess-the-number> python guessthenumber.py
Hello, what is your name?
Ravi
Well, Ravi, I am thinking of a number between 1 and 20.
@@ -54,35 +71,166 @@ Take a guess. You have 2 guesses left.
Good job, Ravi! You guessed my number in 5 guesses.
```
-## Explanation
-1. The `import random` statement will import the random module into your program. This will allow you to generate random numbers.
-2. The `print('Hello, what is your name?')` statement will print the string `Hello, what is your name?` to the console.
-3. The `name = input()` statement will prompt the user to enter their name and store the value in the `name` variable.
-4. The `print('Well, ' + name + ', I am thinking of a number between 1 and 20.')` statement will print the string `Well, ` followed by the value of the `name` variable, followed by the string `, I am thinking of a number between 1 and 20.` to the console.
-5. The `secretNumber = random.randint(1, 20)` statement will generate a random number between 1 and 20 and store the value in the `secretNumber` variable.
-6. The `for guessesTaken in range(1, 7):` statement will create a for loop that will run six times. The `guessesTaken` variable will store the number of times the loop has run.
-7. The `print('Take a guess. You have ' + str(7 - guessesTaken) + ' guesses left.')` statement will print the string `Take a guess. You have ` followed by the value of the `guessesTaken` variable subtracted from 7, followed by the string ` guesses left.` to the console.
-8. The `guess = int(input())` statement will prompt the user to enter a number and store the value in the `guess` variable.
-9. The `if guess < secretNumber:` statement will check if the value of the `guess` variable is less than the value of the `secretNumber` variable.
-10. The `print('Your guess is too low.')` statement will print the string `Your guess is too low.` to the console.
-11. The `elif guess > secretNumber:` statement will check if the value of the `guess` variable is greater than the value of the `secretNumber` variable.
-12. The `print('Your guess is too high.')` statement will print the string `Your guess is too high.` to the console.
-13. The `else:` statement will run if the `if` and `elif` statements are false.
-14. The `break` statement will end the loop.
-15. The `if guess == secretNumber:` statement will check if the value of the `guess` variable is equal to the value of the `secretNumber` variable.
-16. The `print('Good job, ' + name + '! You guessed my number in ' + str(guessesTaken) + ' guesses.')` statement will print the string `Good job, ` followed by the value of the `name` variable, followed by the string `! You guessed my number in `, followed by the value of the `guessesTaken` variable, followed by the string ` guesses.` to the console.
+Run it a few times. The secret number changes every time.
+
+## Step-by-Step Explanation
+
+### 1. Import randomness
+```python title="guessthenumber.py"
+import random
+```
+The `random` module is part of Python's standard library. After this line, `random.randint(...)` is available.
+
+### 2. Greet the user
+```python title="guessthenumber.py"
+print('Hello, what is your name?')
+name = input()
+print('Well, ' + name + ', I am thinking of a number between 1 and 20.')
+```
+- `input()` waits for the user to press Enter and returns their text as a string.
+- `'Well, ' + name + ', ...'` is **string concatenation** — joining pieces of text with `+`.
+
+> 💡 A more modern alternative is the f-string: `print(f'Well, {name}, I am thinking of a number between 1 and 20.')`
+
+### 3. Pick a secret number
+```python title="guessthenumber.py"
+secretNumber = random.randint(1, 20)
+```
+`random.randint(1, 20)` returns a random integer from 1 to 20 inclusive — both endpoints are possible.
+
+### 4. Loop six times
+```python title="guessthenumber.py"
+for guessesTaken in range(1, 7):
+ print('Take a guess. You have ' + str(7 - guessesTaken) + ' guesses left.')
+ guess = int(input())
+```
+- `range(1, 7)` generates `1, 2, 3, 4, 5, 6` — six iterations.
+- `guessesTaken` is the current attempt number.
+- `7 - guessesTaken` gives the player how many tries remain.
+- `int(input())` reads a line and converts it to an integer. If the user types `"abc"`, this line will crash — we will fix that in the "Robust Input" section below.
+
+### 5. Compare and react
+```python title="guessthenumber.py"
+ if guess < secretNumber:
+ print('Your guess is too low.')
+ elif guess > secretNumber:
+ print('Your guess is too high.')
+ else:
+ break
+```
+The three branches cover every possibility:
+- `<` → too low, keep going.
+- `>` → too high, keep going.
+- otherwise (`==`) → correct, `break` out of the loop.
+
+### 6. Final message
+```python title="guessthenumber.py"
+if guess == secretNumber:
+ print('Good job, ' + name + '! You guessed my number in ' + str(guessesTaken) + ' guesses.')
+else:
+ print('Nope. The number I was thinking of was ' + str(secretNumber))
+```
+If the loop ended because of `break`, the player won. Otherwise they ran out of guesses.
+
+## Strategy: How Many Guesses Should You Need?
+
+With six guesses for a range of 20, the optimal strategy is **binary search** — always guess the middle. Each guess halves the remaining range:
+
+| Guess # | Range | Mid |
+|---|---|---|
+| 1 | 1–20 | 10 |
+| 2 | half | 5 or 15 |
+| 3 | quarter | … |
+| 4–5 | usually solved | |
+
+In general you need about `log₂(n)` guesses for a range of `n` numbers. For 1–20 that is about 4.3, so six guesses is generous.
+
+## Robust Input
+
+The basic version crashes when the user types letters. Replace `int(input())` with a helper:
+
+```python title="safe_input.py"
+def ask_number(prompt, low, high):
+ while True:
+ raw = input(prompt)
+ try:
+ n = int(raw)
+ except ValueError:
+ print("Please enter a whole number.")
+ continue
+ if not (low <= n <= high):
+ print(f"Number must be between {low} and {high}.")
+ continue
+ return n
+```
+Now `guess = ask_number("Your guess: ", 1, 20)` handles every bad input gracefully.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `ValueError: invalid literal for int()` | User typed letters | Wrap in `try`/`except ValueError` |
+| Loop ends but program says "Nope" even when correct | Used `==` inside the loop without `break` | Add `break` in the `else` branch |
+| Cannot concatenate `str` and `int` | `'left: ' + guessesTaken` | Convert with `str(guessesTaken)` or use an f-string |
+| Random number is always the same | You set `random.seed(0)` somewhere | Remove the seed call for true variability |
+
+## Variations to Try
+
+### 1. Difficulty levels
+```python title="difficulty.py"
+levels = {"easy": (1, 10, 6), "medium": (1, 50, 7), "hard": (1, 100, 8)}
+choice = input("Difficulty (easy/medium/hard): ").lower()
+low, high, tries = levels[choice]
+secret = random.randint(low, high)
+```
+
+### 2. Play-again loop
+```python title="replay.py"
+while True:
+ play_game()
+ again = input("Play again? (y/n): ").lower()
+ if again != "y":
+ break
+```
+
+### 3. Track best score
+```python title="best.py"
+best = None
+# after each round:
+if best is None or guessesTaken < best:
+ best = guessesTaken
+ print(f"New best score: {best} guesses!")
+```
+
+### 4. Persistent high-score file
+```python title="persist.py"
+from pathlib import Path
+SCORE_FILE = Path("best_score.txt")
+best = int(SCORE_FILE.read_text()) if SCORE_FILE.exists() else None
+# update and write back:
+if best is None or new_score < best:
+ SCORE_FILE.write_text(str(new_score))
+```
+
+### 5. Reverse the game — you pick, the computer guesses
+See the [Number Guessing Game with AI](./numberguessingwithai) project. The AI uses binary search to guess your number in at most seven tries.
+
+### 6. GUI version with Tkinter
+Replace `input()` and `print()` with an entry widget and a label. A few hours of work and you have a clickable game.
+
+## Common Interview-Style Questions This Project Touches
+- "How would you guess a number between 1 and 1,000,000 with the fewest tries?" → binary search.
+- "Why is `int(input())` dangerous?" → invalid user input can crash the program; use `try`/`except`.
+- "How does Python's `random` module work under the hood?" → Mersenne Twister, pseudo-random.
+
+## Real-World Applications
+- Game development (loot tables, RNG events, procedural generation).
+- Educational tools that teach the binary search algorithm visually.
+- A/B testing assignment when you need a random bucket.
+- Quick demos of probability concepts in classrooms.
## Next Steps
-Congratulations! You have successfully created a Guess the Number game in Python. Experiment with the code and see if you can modify the application. Here are a few suggestions.
-- Change the number of guesses.
-- Change the range of numbers.
-- Add a high score.
-- Add a timer.
-- Add a GUI.
-- Add a database to store the high score.
-- Add a leaderboard.
-- Add a multiplayer mode.
-- Add a difficulty setting.
+Pick one of the variations above and ship it. Combine two or three for extra credit — for example, difficulty levels + persistent high-score + play-again loop is a complete tiny game. Then move on to projects that combine randomness with state, such as [Hangman](./hangman) or [Blackjack](./blackjack).
## Conclusion
-In this project, you learned how to create a Guess the Number game in Python. You also learned how to use a while loop with a break statement. You can find the source code [Github](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/guessthenumber.py)
\ No newline at end of file
+You wrote a complete game in roughly fifteen lines of Python, learned how `random.randint` works, looped with `for…range`, branched with `if`/`elif`/`else`, and saw how a small program scales up with input validation and replayability. The full source is on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/guessthenumber.py). Find more projects like this on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/hangman.mdx b/src/content/docs/projects/Beginners/hangman.mdx
index 630b76f2..e053362c 100644
--- a/src/content/docs/projects/Beginners/hangman.mdx
+++ b/src/content/docs/projects/Beginners/hangman.mdx
@@ -1,6 +1,6 @@
---
title: Hangman Game
-description: A classic word guessing game implemented in Python. Players guess letters to complete a hidden word within a limited number of attempts.
+description: Build the classic Hangman word-guessing game in Python. Learn file I/O, sets for guess tracking, ASCII art for the gallows, replay loops, category selection, and how to grow it into a polished game.
sidebar:
order: 13
hero:
@@ -13,180 +13,330 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Hangman is a classic word guessing game where players try to guess a hidden word by suggesting letters within a limited number of attempts. This Python implementation uses a text file containing a list of words and randomly selects one for the player to guess. The game provides feedback on correct and incorrect guesses and tracks the number of remaining attempts. This project is excellent for beginners to learn about file handling, string manipulation, and game logic in Python.
+Hangman is the perfect "second game" project. It is more complex than Rock-Paper-Scissors but still fits in one file. You learn file I/O (loading a word list), random selection, sets (tracking guessed letters), string assembly (showing the partially-revealed word), and game-state management (win, lose, keep going). In this tutorial you will build the classic version with a word list, then layer in ASCII-art gallows, input validation, category selection, score tracking, hints, and a replay loop.
-## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
-- A text file with words (words.txt)
+You will learn:
+- Why `set` is the right structure for "letters already guessed".
+- How to reveal a word letter-by-letter from a hidden secret.
+- How to draw step-by-step ASCII art from a list of stages.
+- How to organize game state into clean functions.
+- How to extend a beginner game with categories, hints, and persistence.
+
+## The Rules
+1. The computer picks a secret word.
+2. The player sees blanks: `_ _ _ _ _ _ _`.
+3. Player guesses one letter at a time.
+4. Correct letters fill in their positions.
+5. Wrong letters cost one "limb" of the hanged figure.
+6. Player wins by completing the word; loses if the figure is fully drawn first.
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download).
+## Prerequisites
+- Python 3.6 or above.
+- A code editor or IDE.
+- A text file of words (we will make one).
+- Comfort with `while` loops, `if/else`, and basic file reading.
## Getting Started
-#### Create a Project
+
+### Create the project
1. Create a folder named `hangman-game`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `hangman.py`.
-4. Create a file named `words.txt` and add some words (one word per line).
-5. Copy the given code and paste it in your `hangman.py` file.
-
-#### Create Words File
-Create a `words.txt` file in the same directory and add words like:
-```txt title="words.txt" showLineNumbers{1} {1-10}
-python
-programming
-computer
-science
-technology
-development
-software
-coding
-algorithm
-function
-```
+2. Inside it, create `hangman.py`.
+3. Create `words.txt` next to it. Suggested starter contents:
+ ```txt title="words.txt"
+ python
+ programming
+ computer
+ science
+ technology
+ development
+ software
+ coding
+ algorithm
+ function
+ variable
+ recursion
+ compiler
+ interpreter
+ ```
-## Write the Code
-1. Copy and paste the following code in your `hangman.py` file.
-
-2. Save the file.
-3. Make sure you have the `words.txt` file in the same directory.
-4. Open the terminal in your code editor or IDE and navigate to the folder `hangman-game`.
-```cmd title="command" showLineNumbers{1} {1-30}
+### Write the code
+
+
+### Run it
+```cmd title="command"
C:\Users\Your Name\hangman-game> python hangman.py
What is your name? John
-Good Luck ! John
-The game is about to start !
-Let's play Hangman
-Guess the characters
-________
-You have 16 more guesses
-guess a character: p
-p_______
-You have 16 more guesses
-guess a character: r
-pr______
-You have 16 more guesses
-guess a character: o
-pro_____
-You have 16 more guesses
-guess a character: g
-prog____
-You have 16 more guesses
-guess a character: r
-progr___
-You have 16 more guesses
-guess a character: a
-progra__
-You have 16 more guesses
-guess a character: m
-program_
-You have 16 more guesses
-guess a character: m
-programm
-You have 15 more guesses
-guess a character: i
-programmi
-You have 15 more guesses
-guess a character: n
-programming
-You Win
-The word is: programming
+Good luck, John! Let's play Hangman.
+Word: _ _ _ _ _ _ _
+Guesses left: 6
+Guess a letter: p
+Word: p _ _ _ _ _ _
+Guess a letter: ...
```
-## Explanation
-1. Import the required modules.
-```python title="hangman.py" showLineNumbers{1} {1-3}
+## Step-by-Step Explanation
+
+### 1. Imports
+```python title="hangman.py"
import random
import time
```
+`random.choice` picks the word. `time.sleep` adds drama.
+
+### 2. Load and pick a word
+```python title="hangman.py"
+with open("words.txt", encoding="utf-8") as f:
+ words = [line.strip().lower() for line in f if line.strip()]
+word = random.choice(words)
+```
+Reading inside a `with` block ensures the file is closed automatically. The list comprehension strips trailing newlines and skips empty lines.
-2. Get the player's name and provide welcome messages.
-```python title="hangman.py" showLineNumbers{1} {1-10}
-name = input("What is your name? ")
-print("Good Luck ! ", name)
+### 3. Game state
+```python title="hangman.py"
+guessed_letters = set()
+wrong = 0
+max_wrong = 6
+```
+Why a **set** for `guessed_letters`?
+- `letter in guessed_letters` is O(1).
+- Adding a duplicate is harmless.
+- It mirrors the natural language of the rules — *"have I guessed this letter already?"*
+
+### 4. Reveal the secret partially
+```python title="hangman.py"
+def display(word: str, guessed: set[str]) -> str:
+ return " ".join(c if c in guessed else "_" for c in word)
+```
+A list/generator comprehension over the secret word: keep characters the player has guessed, otherwise show `_`. `" ".join(...)` spaces them out for readability.
-time.sleep(1)
+### 5. The main loop
+```python title="hangman.py"
+while wrong < max_wrong:
+ print(f"\n{display(word, guessed_letters)}")
+ print(f"Guesses left: {max_wrong - wrong}")
+ guess = input("Guess a letter: ").lower().strip()
-print("The game is about to start !")
-print("Let's play Hangman")
+ if len(guess) != 1 or not guess.isalpha():
+ print("Enter a single letter.")
+ continue
+ if guess in guessed_letters:
+ print("Already guessed.")
+ continue
-time.sleep(1)
+ guessed_letters.add(guess)
+ if guess in word:
+ if all(c in guessed_letters for c in word):
+ print(f"\n🎉 You win! The word was '{word}'.")
+ break
+ else:
+ wrong += 1
+ print("Wrong!")
+else:
+ print(f"\nYou lost. The word was '{word}'.")
```
+Several patterns in play:
+- **Input validation** rejects multi-char or non-alphabetic input.
+- **Duplicate detection** uses the set.
+- **Win check** asks *"are every letter of the word now in `guessed`?"* — `all(...)` is the right tool.
+- **`else` clause on a `while`** runs only if the loop ended normally (the player ran out of guesses). It is a lesser-known but elegant feature of Python.
-3. Read words from the file and select a random word.
-```python title="hangman.py" showLineNumbers{1} {1-5}
-open_file = open("words.txt", "r")
-words = open_file.readlines()
-word = random.choice(words)
-word = word.lower().strip().replace("\n", "").replace("\r", "").replace(" ", "").replace("-", "")
+## ASCII-Art Gallows
+
+A 7-frame gallows that draws progressively as the player loses:
+
+```python title="gallows.py"
+STAGES = [
+ r"""
+ -----
+ | |
+ |
+ |
+ |
+ |
+ =========""",
+ r"""
+ -----
+ | |
+ O |
+ |
+ |
+ |
+ =========""",
+ r"""
+ -----
+ | |
+ O |
+ | |
+ |
+ |
+ =========""",
+ r"""
+ -----
+ | |
+ O |
+ /| |
+ |
+ |
+ =========""",
+ r"""
+ -----
+ | |
+ O |
+ /|\ |
+ |
+ |
+ =========""",
+ r"""
+ -----
+ | |
+ O |
+ /|\ |
+ / |
+ |
+ =========""",
+ r"""
+ -----
+ | |
+ O |
+ /|\ |
+ / \ |
+ |
+ =========""",
+]
+
+print(STAGES[wrong]) # in the main loop
+```
+Six wrong guesses → seven stages (0 wrong = empty gallows, 6 wrong = full hangman). The `r"""..."""` is a raw triple-quoted string — backslashes inside stay literal.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| Word contains a newline character | Forgot `.strip()` after `readlines()` | Use the list-comp above |
+| Capital letters never match | Mixed cases | Lowercase both the word and the guess |
+| Pressing Enter empty crashes the game | No length check | Validate `len(guess) == 1` |
+| Counting wrong letters when a duplicate is pressed | No duplicate check | Skip already-guessed letters |
+| Win never triggers | Used `==` instead of `all(... in ...)` | Use `all(c in guessed_letters for c in word)` |
+
+## Variations to Try
+
+### 1. Categories
+Store one word list per category:
+```
+fruits.txt
+animals.txt
+programming.txt
```
+Ask the user to pick a category. Each `.txt` becomes a `Path`:
+```python title="categories.py"
+from pathlib import Path
+cats = {p.stem: p for p in Path("words").glob("*.txt")}
+choice = input(f"Pick: {', '.join(cats)}\n> ")
+words = cats[choice].read_text().splitlines()
+```
+
+### 2. Difficulty levels
+- **Easy:** 4–6 letter words, 8 guesses.
+- **Medium:** 7–9 letter words, 6 guesses.
+- **Hard:** 10+ letter words, 5 guesses, only common letters give hints.
-4. Initialize game variables.
-```python title="hangman.py" showLineNumbers{1} {1-3}
-print("Guess the characters")
-guesses = ''
-turns = len(word) * 2
+### 3. Hints
+Give a hint after N wrong guesses — reveal one letter automatically:
+```python title="hint.py"
+if wrong == 4:
+ for c in word:
+ if c not in guessed_letters:
+ print(f"Hint: contains '{c}'")
+ guessed_letters.add(c)
+ break
```
-5. Main game loop - check guesses and display progress.
-```python title="hangman.py" showLineNumbers{1} {1-20}
-while turns > 0:
- failed = 0
- for char in word:
- if char in guesses:
- print(char, end="")
- else:
- print("_", end="")
- failed += 1
-
- if failed == 0:
- print("\nYou Win")
- print("The word is: ", word)
+### 4. Score tracking
+A win earns `(letters - wrong) * 10` points. Persist between sessions in a JSON file. See [Personal Diary](./personaldiary) for the same persistence pattern.
+
+### 5. Replay loop
+```python title="replay.py"
+while True:
+ play_round()
+ if input("Play again? (y/n): ").lower() != "y":
break
```
-6. Handle user input and game logic.
-```python title="hangman.py" showLineNumbers{1} {1-15}
-guess = input("guess a character: ")
-guesses += guess
-
-if guess not in word:
- turns -= 1
- print("Wrong")
-
- if turns == 0:
- print("\nYou Loose")
- print("\nThe word is: ", word)
- break
+### 6. Two-player mode
+Player 1 enters the word secretly (`getpass.getpass` hides it from the terminal); Player 2 guesses.
+
+### 7. GUI version
+Tkinter with a letter-grid of buttons and an image-based gallows. See [Basic Music Player](./basicmusicplayer) for the GUI pattern.
+
+### 8. Web version
+Flask plus a single HTML page. The state lives in the user's session. See [Basic Web Server](./basicwebserver).
+
+### 9. Word source from an API
+Pull a random English word from a dictionary API instead of `words.txt`:
+```python title="api_word.py"
+import requests
+word = requests.get("https://random-word-api.herokuapp.com/word").json()[0]
```
-## Features
-- Random word selection from a text file
-- Limited number of guesses based on word length
-- Real-time progress display with underscores
-- Win/lose conditions with appropriate messages
-- Input validation and feedback
-- Clean and intuitive user interface
-
-## How to Play
-1. Run the program and enter your name
-2. The game will display underscores representing each letter of the hidden word
-3. Guess letters one at a time
-4. Correct guesses reveal the letter's position(s) in the word
-5. Incorrect guesses reduce your remaining attempts
-6. Win by guessing all letters before running out of attempts
-7. Lose if you exhaust all attempts without completing the word
+### 10. Definition reveal
+After the round, look up the word's definition using `pip install nltk` + the WordNet corpus, and show it to teach vocabulary.
+
+## Refactoring with a Class
+
+For a serious version, wrap state in a class:
+
+```python title="hangman_class.py"
+class Hangman:
+ def __init__(self, word: str, max_wrong: int = 6):
+ self.word = word.lower()
+ self.guessed: set[str] = set()
+ self.wrong = 0
+ self.max_wrong = max_wrong
+
+ def guess(self, letter: str) -> str:
+ letter = letter.lower()
+ if letter in self.guessed:
+ return "duplicate"
+ self.guessed.add(letter)
+ if letter not in self.word:
+ self.wrong += 1
+ return "miss"
+ return "hit"
+
+ @property
+ def display(self) -> str:
+ return " ".join(c if c in self.guessed else "_" for c in self.word)
+
+ @property
+ def is_won(self) -> bool:
+ return all(c in self.guessed for c in self.word)
+
+ @property
+ def is_lost(self) -> bool:
+ return self.wrong >= self.max_wrong
+```
+Now the game logic is testable without `input()` — perfect for unit tests.
+
+## Real-World Applications
+- **Vocabulary teaching** — a hangman with curated word lists works in language learning apps.
+- **CAPTCHA-style verification** in a friendly form.
+- **Conference / classroom icebreaker** — group hangman on a projector.
+- **Onboarding gamification** — turn product terminology into a guessing game.
+
+## Educational Value
+This project teaches:
+- **File I/O** — reading lists from text files.
+- **Sets** — O(1) membership tests and natural-language semantics.
+- **String manipulation** — partial reveal, comparison, validation.
+- **Game-state management** — wrong count, guessed letters, win/lose conditions.
+- **Refactoring** — moving from a script to a class for testability.
## Next Steps
-You can enhance this project by:
-- Adding a graphical hangman drawing for wrong guesses
-- Implementing difficulty levels with different word categories
-- Adding a scoring system based on guesses used
-- Creating a GUI version using Tkinter
-- Adding hint functionality
-- Implementing multiplayer mode
-- Adding word definitions or categories
+- Build the **ASCII-art version** with the `STAGES` list above.
+- Add **categories** and **difficulty levels**.
+- Wrap state in a **`Hangman` class** and add **unit tests** with `pytest`.
+- Build a **GUI** or **web** version.
+- Use a **real word API** so the dictionary never runs out.
## Conclusion
-In this project, we learned how to create a Hangman game using Python. We covered file handling to read words from a text file, string manipulation for processing the selected word, and game logic for tracking guesses and determining win/lose conditions. This project demonstrates essential programming concepts like loops, conditionals, and user input handling. To find more projects like this, you can visit Python Central Hub.
+You built a complete Hangman game from a 50-line script: file loading, secret picking, partial reveal, input validation, win/lose conditions, and ASCII-art gallows. The same patterns power dozens of guessing games and quiz apps. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/hangman.py). Explore more games on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/helloworld.mdx b/src/content/docs/projects/Beginners/helloworld.mdx
index 44a8e14a..e87901a6 100644
--- a/src/content/docs/projects/Beginners/helloworld.mdx
+++ b/src/content/docs/projects/Beginners/helloworld.mdx
@@ -1,6 +1,6 @@
---
title: Hello World Project
-description: A simple Hello World project to get you started with learning from projects.
+description: A complete beginner's guide to writing, running, and understanding your very first Python program. Learn how Python executes code, what `print()` does behind the scenes, and how to extend your first program.
sidebar:
order: 1
hero:
@@ -14,30 +14,174 @@ import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Creating a "Hello World" project is a classic and essential first step for anyone learning a new programming language. In this guide, we'll walk through the process of building a simple "Hello World" project using Python. This project serves as a foundation for understanding basic syntax, setting up a development environment, and executing a Python script.
+Writing a "Hello, World!" program is the traditional first step in learning any programming language. It looks trivial, but it teaches you four very real things at once: how to **write** source code, how to **save** it as a file your machine can find, how to **run** it through the Python interpreter, and how to **read** the output. In this tutorial we will go beyond just typing one line. We will explore *why* the program works, what happens when you press Enter, common errors beginners hit on their first day, and several small variations that take you from a single print statement to your first interactive program.
+
+By the end of this tutorial you will be able to:
+- Verify Python is installed on your computer.
+- Create and save a `.py` file.
+- Run a script from the terminal.
+- Understand what `print()` does and how Python evaluates a line of code.
+- Modify the program to take user input and respond.
## Prerequisites
-- Python 3.6 or above
-- Text Editor or IDE (Visual Studio Code, PyCharm, etc.)
+- Python 3.6 or above installed ([download page](https://www.python.org/downloads/)).
+- A text editor or IDE. We recommend [Visual Studio Code](https://code.visualstudio.com/) with the official Python extension.
+- A terminal you are comfortable opening (Command Prompt or PowerShell on Windows, Terminal on macOS/Linux).
+- No previous programming experience required.
+
+## Before You Start
+Open a terminal and confirm Python is on your PATH:
+
+```cmd title="check-python"
+python --version
+```
+
+You should see something like `Python 3.11.4`. If you get *"python is not recognized"* on Windows, re-run the Python installer and check the **"Add Python to PATH"** option, then restart your terminal. On macOS/Linux you may need to use `python3` instead of `python`.
## Getting Started
-### Create a new project
-1. Open Text Editor or IDE
-2. Select **File** > **New File** from the menu bar
-3. Select **File** > **Save As** from the menu bar
-4. Name the file `hello_world.py`
-5. Select **File** > **Save** from the menu bar
-
-### Write the code
-1. Add the following code to the file:
+
+### Step 1 — Create a project folder
+Keeping each project in its own folder is a habit worth forming on day one. It keeps your scripts organized and prevents naming clashes later.
+
+1. Choose a place on your disk, for example `Documents`.
+2. Create a new folder named `hello-world`.
+3. Open the folder in your editor (in VS Code: **File → Open Folder…**).
+
+### Step 2 — Create the file
+1. In your editor, choose **File → New File**.
+2. Save it immediately with **File → Save As…** as `hello_world.py` inside the `hello-world` folder.
+3. The `.py` extension is important — it tells the operating system and your editor that this is a Python file. Editors use the extension to enable syntax highlighting and Python tooling.
+
+### Step 3 — Write the code
+Add the following code to `hello_world.py`:
-2. Select **File** > **Save** from the menu bar
-3. Select **Terminal** > **New Terminal** from the menu bar
-4. In the terminal,
+
+Save the file again with **Ctrl+S** (or **Cmd+S** on macOS).
+
+### Step 4 — Run it
+Open the integrated terminal in your editor (**View → Terminal** in VS Code) and run:
+
```cmd title="command" showLineNumbers{1} {2}
-C:\Users\username\Documents\hello_world> python hello_world.py
+C:\Users\username\Documents\hello-world> python hello_world.py
Hello World!
```
+That `Hello World!` line is your program's output, printed to the screen by the Python interpreter.
+
+## How It Works
+
+A Python source file is just a plain text file. When you type `python hello_world.py`, four things happen behind the scenes:
+
+1. **The interpreter starts.** Your operating system launches the `python` executable.
+2. **It reads your file top-to-bottom.** Python parses the file into tokens (keywords, names, punctuation), then into an internal tree representation called an **AST** (Abstract Syntax Tree).
+3. **It compiles to bytecode.** The AST is compiled into a compact intermediate format. You may notice a `__pycache__` folder appear next to your script — that is the cached bytecode.
+4. **It executes the bytecode.** The Python virtual machine walks the bytecode, sees a call to the built-in `print` function with the string `"Hello World!"`, and writes that string to **standard output** — which your terminal then displays.
+
+The whole pipeline is invisible. From your perspective: you saved a file, ran one command, and saw output. Everything else is Python's job.
+
+### What does `print()` actually do?
+`print` is a **built-in function**. You can call it with any value:
+
+```python title="print-examples"
+print("Hello World!") # a string
+print(42) # an integer
+print(3.14 * 2) # an expression that evaluates first
+print("a", "b", "c") # multiple values separated by spaces
+```
+
+By default `print` appends a newline character (`\n`) after its output. You can change this with the `end` keyword argument:
+
+```python title="end-arg"
+print("Hello", end=" ")
+print("World!")
+# Output: Hello World!
+```
+
+## Common Mistakes
+
+Beginners often hit one of these on day one. None of them are signs you are bad at programming — every Python developer has seen them.
+
+| Problem | What you typed | What went wrong | Fix |
+|---|---|---|---|
+| `SyntaxError: Missing parentheses in call to 'print'` | `print "Hello"` | Python 2 syntax | Use `print("Hello")` |
+| `NameError: name 'Hello' is not defined` | `print(Hello)` | Missing quotes — Python looked for a variable named `Hello` | Wrap strings in quotes: `print("Hello")` |
+| `python: can't open file 'hello_world.py'` | Ran `python hello_world.py` from wrong folder | Terminal is in a different directory | `cd` into your project folder first |
+| Nothing happens | Forgot to **save** the file | Editor still shows the dot meaning unsaved changes | Save with **Ctrl+S** before running |
+| `IndentationError` | Pasted code with leading spaces | Python is whitespace-sensitive | Remove leading whitespace on top-level statements |
+
+## Variations to Try
+
+Once your first program runs, modify it. Tweaking working code is the fastest way to learn.
+
+### 1. Ask for a name
+```python title="hello_user.py"
+name = input("What is your name? ")
+print("Hello,", name + "!")
+```
+- `input()` pauses the program, waits for the user to type something and press Enter, and returns whatever they typed as a string.
+- `name + "!"` joins two strings using the `+` operator (called **string concatenation**).
+
+### 2. Use an f-string
+F-strings are the modern, readable way to embed values inside strings:
+```python title="f-string.py"
+name = input("Name: ")
+print(f"Hello, {name}! Welcome to Python.")
+```
+The `f` prefix tells Python: *"this string contains expressions in curly braces — evaluate them and insert the result."*
+
+### 3. Greet in multiple languages
+```python title="multilang.py"
+print("Hello, World!")
+print("Hola, Mundo!")
+print("Bonjour le monde!")
+print("こんにちは世界")
+```
+Python source files are UTF-8 by default — non-Latin characters work out of the box.
+
+### 4. Loop a greeting
+```python title="loop.py"
+for i in range(3):
+ print(f"Hello #{i + 1}")
+```
+This introduces the `for` loop and `range()` — two building blocks you will use in nearly every program.
+
+## Running Without the Terminal
+
+If you prefer not to use the terminal:
+
+- **VS Code:** click the ▶ run button in the top-right while `hello_world.py` is open.
+- **PyCharm:** right-click the file in the project tree → **Run 'hello_world'**.
+- **IDLE** (bundled with Python): open the file and press **F5**.
+
+All three do the same thing — they invoke `python hello_world.py` for you and capture the output in a panel.
+
+## Frequently Asked Questions
+
+**Why is it always "Hello, World!"?**
+The phrase comes from Brian Kernighan's 1972 tutorial for the B programming language and was popularized by the 1978 *C Programming Language* book. It became a tradition.
+
+**Do I need to compile Python like C or Java?**
+No. Python compiles to bytecode automatically the first time the file is run and caches it in `__pycache__`. You only run `python hello_world.py`.
+
+**What if I want to share this with someone who does not have Python installed?**
+Look into **PyInstaller** or **Nuitka** — both can package a Python script into a standalone executable. That is far beyond Hello World, but good to know it is possible.
+
+## Educational Value
+Even this tiny program teaches:
+- **File-based source code** — the foundation of every real Python project.
+- **The print function** — output is how programs communicate with humans.
+- **String literals** — your first data type.
+- **Running an interpreter** — the loop you will use thousands of times: edit, save, run, read output, fix.
+- **Reading errors** — error messages are not punishments, they are clues.
+
## Next Steps
-Congratulations! You've successfully created a "Hello World" project using Python. Now that you've completed this project, you can continue learning Python by completing the next project in the series on Python Central Hub. You can find the complete source code of this project on [Github](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/helloworld.py).
+You have a working Python development setup and you understand what happened when you ran your first program. From here, try one of the following:
+
+- Modify the program to greet *you* by name.
+- Move on to the next project, **Simple Calculator**, which introduces functions and basic arithmetic.
+- Read about [Python's built-in functions](https://docs.python.org/3/library/functions.html) — `print` is just one of about seventy.
+
+You can find the complete source code on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/helloworld.py).
+
+## Conclusion
+A one-line program is not the goal. The goal is to make sure your environment works end-to-end: editor saves, interpreter runs, terminal shows output. Once that loop is reliable, every other project on Python Central Hub becomes easier. Celebrate this moment — every programmer alive started exactly where you are right now.
diff --git a/src/content/docs/projects/Beginners/json-data-validator.mdx b/src/content/docs/projects/Beginners/json-data-validator.mdx
index fa7bdd76..abed134c 100644
--- a/src/content/docs/projects/Beginners/json-data-validator.mdx
+++ b/src/content/docs/projects/Beginners/json-data-validator.mdx
@@ -1,6 +1,6 @@
---
title: JSON Data Validator
-description: A comprehensive JSON schema validation tool with custom schema support, batch validation, error reporting, and data generation capabilities
+description: Build a Python JSON Schema validator from scratch. Learn type/constraint validation, path-tracked errors, batch validation, sample-data generation, and how it compares to jsonschema and pydantic.
sidebar:
order: 50
hero:
@@ -13,377 +13,306 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+JSON is the data format of the modern web — APIs, configs, telemetry, NoSQL stores all live in it. But JSON gives you no type safety: a misnamed field or out-of-range number sails through `json.loads()` and silently breaks downstream code hours later. A **schema validator** catches those errors at the boundary, with precise error messages pointing at the offending field. In this project you build one from scratch — type checking, constraints, nested validation, path-tracked errors, batch validation, and sample-data generation — then compare it to the production-grade libraries `jsonschema` and `pydantic`.
-A comprehensive JSON schema validation tool that validates JSON data against custom schemas, provides detailed error reporting, supports batch validation, and includes data generation capabilities. This project demonstrates advanced data validation, schema design, and file processing techniques.
+You will leave understanding:
+- The JSON Schema specification (the subset most apps actually use).
+- How to write a small recursive validator.
+- Path-tracked error messages (`root.users[0].email`).
+- The difference between schema-based and class-based validation.
+- When to graduate to `jsonschema` (for spec compliance) or `pydantic` (for type-driven validation).
-## 🎯 Project Overview
+## Prerequisites
+- Python 3.7 or above (for `dataclasses`).
+- A text editor or IDE.
+- Familiarity with JSON, dictionaries, and recursive functions.
-This project creates a powerful JSON validation system with:
-- Custom JSON schema definition and validation
-- File and string validation capabilities
-- Batch validation for multiple files
-- Detailed error reporting with path information
-- Sample data generation from schemas
-- Validation statistics and reporting
-- Interactive command-line interface
+## Getting Started
-## ✨ Features
+### Create the project
+1. Create folder `json-validator`.
+2. Inside, create `jsondatavalidator.py`.
-### Schema Management
-- **Custom Schemas**: Define and load custom JSON schemas
-- **File-based Schemas**: Load schemas from JSON files
-- **Built-in Schemas**: Pre-defined schemas for common data types
-- **Schema Information**: View detailed schema structure and requirements
+### Write the code
+
-### Validation Capabilities
-- **File Validation**: Validate JSON files against schemas
-- **String Validation**: Validate JSON strings directly
-- **Batch Validation**: Validate multiple files with pattern matching
-- **Real-time Validation**: Immediate feedback with detailed error messages
-
-### Error Reporting
-- **Detailed Errors**: Path-based error reporting with context
-- **Error Categories**: Type mismatches, missing fields, constraint violations
-- **Validation History**: Track all validation attempts and results
-- **Export Reports**: Generate comprehensive validation reports
+### Run it
+```bash title="run"
+python jsondatavalidator.py
+```
-### Data Generation
-- **Sample Data**: Generate valid sample data from schemas
-- **Schema Analysis**: Understand schema structure and requirements
-- **Testing Support**: Create test data for development and testing
+```
+1. Validate file
+2. Validate string
+3. Batch validate
+4. Generate sample
+5. Statistics
+6. Quit
+> 1
+Schema: user
+File: data.json
+✅ Valid
+```
-## 🛠️ Technical Implementation
+## A Tiny JSON Schema Example
-### Class Structure
-```python title="jsondatavalidator.py" showLineNumbers{1}
-class ValidationError:
- def __init__(self, path, message, expected=None, actual=None):
- # Detailed error information with context
- # Path tracking for nested validation errors
-
-class JSONSchema:
- def __init__(self, schema):
- # Schema definition and validation logic
- # Recursive validation for nested structures
-
- def validate(self, data, path="root"):
- # Main validation entry point
- # Returns validation status and error list
-
-class JSONDataValidator:
- def __init__(self):
- # Validator management and schema storage
- # Validation history and statistics tracking
+```json title="user_schema.json"
+{
+ "type": "object",
+ "required": ["name", "email", "age"],
+ "properties": {
+ "name": {"type": "string", "minLength": 2, "maxLength": 50},
+ "email": {"type": "string", "pattern": "^.+@.+\\..+$"},
+ "age": {"type": "integer", "minimum": 0, "maximum": 150},
+ "status": {"type": "string", "enum": ["active", "inactive", "pending"]},
+ "tags": {"type": "array", "items": {"type": "string"}, "maxItems": 10}
+ }
+}
+```
+Matching data:
+```json title="user.json"
+{"name": "Madhur", "email": "m@example.com", "age": 30, "status": "active"}
```
-### Key Components
-
-#### Schema Validation
-- **Type Checking**: Validate data types (string, number, boolean, array, object)
-- **Constraint Validation**: Check length, range, pattern, and enum constraints
-- **Required Fields**: Ensure all required fields are present
-- **Nested Validation**: Recursive validation for complex data structures
-
-#### Error Handling
-- **Path Tracking**: Precise error location with JSON path notation
-- **Context Information**: Expected vs. actual value reporting
-- **Error Aggregation**: Collect all validation errors in single pass
-- **User-friendly Messages**: Clear, actionable error descriptions
-
-#### Data Processing
-- **JSON Parsing**: Robust JSON file and string parsing
-- **File Operations**: Safe file handling with error recovery
-- **Pattern Matching**: Glob pattern support for batch operations
-- **Data Serialization**: Export validation results and reports
-
-## 🚀 How to Run
-
-1. **Install Python**: Ensure Python 3.7+ is installed (uses built-in modules)
-2. **Run the Validator**:
- ```bash
- python jsondatavalidator.py
- ```
-
-3. **Getting Started**:
- - Explore pre-loaded sample schemas
- - Validate sample JSON files
- - Create custom schemas for your data
- - Generate sample data for testing
-
-## 💡 Usage Examples
-
-### Basic Validation
-```python title="jsondatavalidator.py" showLineNumbers{1}
-# Create validator instance
-validator = JSONDataValidator()
-
-# Add a simple schema
-user_schema = {
- "type": "object",
- "required": ["name", "email"],
- "properties": {
- "name": {"type": "string", "minLength": 2},
- "email": {"type": "string", "pattern": r"^.+@.+\..+$"}
- }
-}
-validator.add_schema("user", user_schema)
+## Step-by-Step Explanation
-# Validate data
-user_data = {"name": "John", "email": "john@example.com"}
-is_valid, errors = validator.validate_data(user_data, "user")
+### 1. The error type
+```python title="error.py"
+from dataclasses import dataclass
-if is_valid:
- print("✅ Validation successful!")
-else:
- for error in errors:
- print(f"❌ {error}")
+@dataclass
+class ValidationError:
+ path: str # e.g. "root.users[0].email"
+ message: str # e.g. "expected string, got int"
+ expected: str = ""
+ actual: str = ""
+
+ def __str__(self):
+ return f"{self.path}: {self.message}" + (
+ f" (expected {self.expected}, got {self.actual})"
+ if self.expected else "")
```
+Path-tracked errors are the single biggest UX improvement over a plain `True/False` validator. Users learn *what* is wrong *where*.
-### File Validation
-```python title="jsondatavalidator.py" showLineNumbers{1}
-# Validate JSON file
-is_valid, errors = validator.validate_file("data.json", "user")
+### 2. The validator
+```python title="validator.py"
+import re
+
+TYPE_MAP = {
+ "string": str, "integer": int, "number": (int, float),
+ "boolean": bool, "array": list, "object": dict, "null": type(None),
+}
-# Batch validate multiple files
-results = validator.batch_validate("data/*.json", "user")
-for file_path, (is_valid, errors) in results.items():
- print(f"{file_path}: {'✅' if is_valid else '❌'} ({len(errors)} errors)")
+def validate(data, schema, path="root") -> list[ValidationError]:
+ errors = []
+ expected_type = schema.get("type")
+ if expected_type:
+ py_type = TYPE_MAP.get(expected_type)
+ if py_type and not isinstance(data, py_type):
+ errors.append(ValidationError(path, "wrong type",
+ expected=expected_type,
+ actual=type(data).__name__))
+ return errors # type mismatch — skip further checks
+
+ if expected_type == "string":
+ if "minLength" in schema and len(data) < schema["minLength"]:
+ errors.append(ValidationError(path, f"shorter than {schema['minLength']}"))
+ if "maxLength" in schema and len(data) > schema["maxLength"]:
+ errors.append(ValidationError(path, f"longer than {schema['maxLength']}"))
+ if "pattern" in schema and not re.match(schema["pattern"], data):
+ errors.append(ValidationError(path, f"does not match pattern {schema['pattern']}"))
+ if "enum" in schema and data not in schema["enum"]:
+ errors.append(ValidationError(path,
+ f"not in {schema['enum']}", actual=str(data)))
+
+ elif expected_type in ("integer", "number"):
+ if "minimum" in schema and data < schema["minimum"]:
+ errors.append(ValidationError(path, f"less than {schema['minimum']}"))
+ if "maximum" in schema and data > schema["maximum"]:
+ errors.append(ValidationError(path, f"greater than {schema['maximum']}"))
+
+ elif expected_type == "array":
+ if "minItems" in schema and len(data) < schema["minItems"]:
+ errors.append(ValidationError(path, f"fewer than {schema['minItems']} items"))
+ if "maxItems" in schema and len(data) > schema["maxItems"]:
+ errors.append(ValidationError(path, f"more than {schema['maxItems']} items"))
+ if "uniqueItems" in schema and len(set(map(repr, data))) != len(data):
+ errors.append(ValidationError(path, "items not unique"))
+ if "items" in schema:
+ for i, item in enumerate(data):
+ errors += validate(item, schema["items"], f"{path}[{i}]")
+
+ elif expected_type == "object":
+ for key in schema.get("required", []):
+ if key not in data:
+ errors.append(ValidationError(path, f"missing required '{key}'"))
+ for key, sub in schema.get("properties", {}).items():
+ if key in data:
+ errors += validate(data[key], sub, f"{path}.{key}")
+ if schema.get("additionalProperties") is False:
+ extras = set(data) - set(schema.get("properties", {}))
+ for k in extras:
+ errors.append(ValidationError(path, f"unexpected property '{k}'"))
+
+ return errors
+```
+Two key design points:
+- **One recursive function** handles every level. Nested schemas validate themselves naturally.
+- **Errors accumulate** rather than short-circuiting. Users see *all* problems at once.
+
+### 3. Validate a file
+```python title="validate_file.py"
+import json
+from pathlib import Path
+
+def validate_file(path: str, schema: dict):
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
+ errors = validate(data, schema)
+ return (not errors), errors
+
+ok, errs = validate_file("user.json", user_schema)
+print("Valid!" if ok else "\n".join(str(e) for e in errs))
```
-### Schema Management
-```python title="jsondatavalidator.py" showLineNumbers{1}
-# Load schema from file
-validator.load_schema_from_file("user_schema.json", "user")
+### 4. Batch validate with glob patterns
+```python title="batch.py"
+def batch(pattern: str, schema: dict):
+ for p in Path().glob(pattern):
+ ok, errs = validate_file(str(p), schema)
+ print(f"{'✅' if ok else '❌'} {p} ({len(errs)} errors)")
-# Get schema information
-schema_info = validator.get_schema_info("user")
-print(json.dumps(schema_info, indent=2))
+batch("data/*.json", user_schema)
+```
-# Generate sample data
-sample_data = validator.create_sample_data("user")
-print(json.dumps(sample_data, indent=2))
+### 5. Generate sample data
+Useful for fixtures and tests:
+```python title="sample.py"
+def sample(schema: dict):
+ t = schema.get("type")
+ if t == "string": return schema.get("enum", ["example"])[0]
+ if t == "integer": return schema.get("minimum", 0)
+ if t == "number": return float(schema.get("minimum", 0))
+ if t == "boolean": return True
+ if t == "array": return [sample(schema["items"])] if "items" in schema else []
+ if t == "object":
+ return {k: sample(v) for k, v in schema.get("properties", {}).items()}
+ return None
```
+Run on a schema → get a fully-typed valid JSON example for documentation or tests.
-## 🎨 Interactive Features
+## Common Mistakes
-### Main Menu Options
-1. **Validate JSON File** - Validate single file against schema
-2. **Validate JSON String** - Validate JSON input directly
-3. **Batch Validate Files** - Validate multiple files with patterns
-4. **Load Schema from File** - Import schema from JSON file
-5. **Add Schema Manually** - Define schema interactively
-6. **View Schema Info** - Explore schema structure
-7. **Generate Sample Data** - Create valid sample data
-8. **View Validation Statistics** - Analytics dashboard
-9. **Export Validation Report** - Generate detailed reports
-10. **List Available Schemas** - View loaded schemas
+| Problem | Cause | Fix |
+|---|---|---|
+| `True` passes `"type": "integer"` check | Python's `bool` is a subclass of `int` | Check `type(x) is int`, not `isinstance` |
+| Pattern always matches | Used `re.match` for partial matches | Use `re.fullmatch` for full-string match |
+| Error message says "root" only | Did not pass path down | Always include `f"{path}.{key}"` in recursive call |
+| Numbers wrongly rejected | Forgot `int + float` for `"number"` | `TYPE_MAP["number"] = (int, float)` |
+| Nested `additionalProperties` not enforced | Only checked at top level | Recurse properly |
+| Schema author typo silently ignored | No schema-of-schemas check | Validate the schema itself before using |
-### Pre-loaded Sample Schemas
+## Comparison to Production Libraries
-#### User Schema
-```json
-{
- "type": "object",
- "required": ["name", "email", "age"],
- "properties": {
- "name": {"type": "string", "minLength": 2, "maxLength": 50},
- "email": {"type": "string", "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"},
- "age": {"type": "integer", "minimum": 0, "maximum": 150},
- "status": {"type": "string", "enum": ["active", "inactive", "pending"]}
- }
-}
+### `jsonschema` (spec-compliant)
+```bash title="install"
+pip install jsonschema
```
-
-#### Product Schema
-```json
-{
- "type": "object",
- "required": ["name", "price", "category"],
- "properties": {
- "name": {"type": "string", "minLength": 1, "maxLength": 100},
- "price": {"type": "number", "minimum": 0},
- "category": {"type": "string", "enum": ["electronics", "clothing", "books"]},
- "tags": {"type": "array", "items": {"type": "string"}, "maxItems": 10}
- }
-}
+```python title="jsonschema_use.py"
+from jsonschema import validate, ValidationError
+try:
+ validate(instance=data, schema=schema)
+except ValidationError as e:
+ print(e.message, e.absolute_path)
```
+Handles full **Draft 2020-12** spec: `$ref`, `oneOf`, `anyOf`, `allOf`, conditional schemas, format checks, custom keywords.
-## 🔧 Advanced Features
-
-### Constraint Types
-
-#### String Constraints
-- **minLength/maxLength**: String length validation
-- **pattern**: Regular expression pattern matching
-- **enum**: Allowed values from predefined list
-
-#### Number Constraints
-- **minimum/maximum**: Numeric range validation
-- **type**: Distinguish between integer and float
-
-#### Array Constraints
-- **minItems/maxItems**: Array length validation
-- **items**: Schema for array elements
-- **uniqueItems**: Ensure array elements are unique
-
-#### Object Constraints
-- **required**: List of required properties
-- **properties**: Schema for object properties
-- **additionalProperties**: Control extra properties
-
-### Error Reporting
-```python title="jsondatavalidator.py" showLineNumbers{1}
-# Example validation error
-ValidationError(
- path="root.users[0].email",
- message="String does not match pattern",
- expected="email pattern",
- actual="invalid-email"
-)
+### `pydantic` (type-driven)
+```bash title="install"
+pip install pydantic
```
+```python title="pydantic_use.py"
+from pydantic import BaseModel, Field, EmailStr
-### Batch Validation
-```python title="jsondatavalidator.py" showLineNumbers{1}
-# Validate all JSON files in directory
-results = validator.batch_validate("data/*.json", "user")
+class User(BaseModel):
+ name: str = Field(min_length=2, max_length=50)
+ email: EmailStr
+ age: int = Field(ge=0, le=150)
-# Advanced pattern matching
-results = validator.batch_validate("**/config.json", "config")
+User.model_validate({"name": "M", "email": "m@x.com", "age": 30})
+# raises ValidationError with errors at .errors()
```
+Schemas *are* Python classes. Faster, friendlier, IDE-autocomplete-aware. Used by FastAPI, LangChain, and most modern Python web stacks.
-## 📊 Statistics and Reporting
+### When to use which
-### Validation Statistics
-```python title="jsondatavalidator.py" showLineNumbers{1}
-{
- 'total_validations': 25,
- 'successful_validations': 20,
- 'failed_validations': 5,
- 'success_rate': 80.0,
- 'schema_usage': {
- 'user': 15,
- 'product': 10
- },
- 'total_errors': 12,
- 'loaded_schemas': ['user', 'product', 'config']
-}
-```
+| Use | Tool |
+|---|---|
+| Quick one-off check | Build it yourself (this project) |
+| Full JSON Schema compliance | `jsonschema` |
+| Pythonic API definitions | `pydantic` |
+| OpenAPI auto-generation | `pydantic` + FastAPI |
+| Schema generation for non-Python consumers | `jsonschema` (export from `pydantic`) |
-### Validation Report
-```json
-{
- "generated_at": "2024-01-01T12:00:00.000000",
- "total_validations": 25,
- "successful_validations": 20,
- "failed_validations": 5,
- "results": [
- {
- "timestamp": "2024-01-01T12:00:00.000000",
- "schema_name": "user",
- "is_valid": true,
- "error_count": 0,
- "errors": []
- }
- ]
-}
-```
+## Variations to Try
-## 🛡️ Error Handling
-
-- **JSON Parsing**: Graceful handling of malformed JSON
-- **File Operations**: Safe file reading with error recovery
-- **Schema Validation**: Comprehensive error checking and reporting
-- **Type Safety**: Runtime type checking and validation
-
-## 📚 Learning Objectives
-
-- **JSON Schema**: Understanding JSON schema specification
-- **Data Validation**: Implementing robust validation logic
-- **Error Handling**: Comprehensive error reporting systems
-- **File Operations**: Safe file handling and processing
-- **Pattern Matching**: Working with glob patterns and regular expressions
-
-## 🎯 Real-world Applications
-
-### API Development
-- Validate API request/response data
-- Ensure data consistency across services
-- Generate API documentation from schemas
-- Create test data for API testing
-
-### Configuration Management
-- Validate application configuration files
-- Ensure required settings are present
-- Check configuration value constraints
-- Generate default configuration templates
-
-### Data Processing
-- Validate data imports and exports
-- Check data quality before processing
-- Ensure data format consistency
-- Generate sample data for testing
-
-## 🚀 Potential Enhancements
-
-1. **JSON Schema Draft Support**: Implement full JSON Schema Draft 7/2019-09
-2. **Custom Validators**: Allow custom validation functions
-3. **Web Interface**: Create Flask web application
-4. **Database Integration**: Store schemas and validation results in database
-5. **CLI Tool**: Command-line interface for automation
-6. **Plugin System**: Extensible validation framework
-7. **Performance Optimization**: Streaming validation for large files
-8. **Schema Registry**: Centralized schema management system
-
-## 🔍 Schema Examples
-
-### Complex Nested Schema
-```python title="jsondatavalidator.py" showLineNumbers{1}
-config_schema = {
- "type": "object",
- "required": ["app_name", "version", "database"],
- "properties": {
- "app_name": {"type": "string", "minLength": 1},
- "version": {"type": "string", "pattern": r"^\d+\.\d+\.\d+$"},
- "database": {
- "type": "object",
- "required": ["host", "port"],
- "properties": {
- "host": {"type": "string"},
- "port": {"type": "integer", "minimum": 1, "maximum": 65535},
- "ssl": {"type": "boolean"}
- }
- },
- "features": {
- "type": "array",
- "items": {"type": "string"},
- "uniqueItems": True
- }
- }
-}
-```
+### 1. Custom keywords
+Add `"isEmail"` or `"isUUID"` as schema-level shortcuts that map to specific patterns.
-### Sample Generated Data
-```json
-{
- "app_name": "sample_string",
- "version": "1.0.0",
- "database": {
- "host": "sample_string",
- "port": 42,
- "ssl": true
- },
- "features": ["sample_string"]
-}
+### 2. `$ref` resolution
+Lets schemas reference each other:
+```json title="ref.json"
+{"address": {"$ref": "#/definitions/address"}}
```
-## 🏆 Project Completion
+### 3. `oneOf` / `anyOf` / `allOf`
+Compose schemas. Email is "string AND matches email pattern AND length ≤ 254".
-This JSON Data Validator demonstrates:
-- ✅ Complete JSON schema validation implementation
-- ✅ Comprehensive error reporting and tracking
-- ✅ Batch processing and file operations
-- ✅ Data generation and schema analysis
-- ✅ Interactive user interface
-- ✅ Statistics and reporting capabilities
+### 4. JSON Schema → markdown docs
+Walk the schema and generate human-readable documentation for an API.
+
+### 5. CLI tool
+```bash title="cli"
+validator schema.json data.json
+validator --watch schema.json incoming/*.json
+```
-Perfect for beginners learning data validation concepts and intermediate developers working with JSON data processing and API development!
+### 6. Web service
+Flask endpoint that accepts schema + data and returns `{"valid": bool, "errors": [...]}`. See [Basic Web Server](./basicwebserver).
+
+### 7. Live validation in a GUI
+Tkinter with a JSON text area and a schema dropdown — errors highlight in real time.
+
+### 8. Streaming validation
+Validate huge JSON Lines files line-by-line with `ijson` to keep memory bounded.
+
+### 9. Schema inference
+Read a sample of valid JSON, *infer* a permissive schema from it. Useful for legacy systems with no docs.
+
+### 10. Mock data generation
+`sample(schema)` already does the basic case. Add `random` for varied output, plus locale-aware names/emails via `Faker`.
+
+## Real-World Applications
+- **API request/response validation** — at every layer of a microservices system.
+- **Config-file validation** — fail fast on bad YAML/JSON at startup.
+- **ETL pipelines** — reject malformed records before they corrupt downstream.
+- **CI / data-quality** — automated checks that incoming data matches contract.
+- **Documentation generation** — schemas become user-facing docs automatically.
+- **OpenAPI / Swagger** — REST APIs document themselves via JSON Schema.
+
+## Educational Value
+- **Recursive algorithms** — schema and data are both trees; you walk them in parallel.
+- **Error reporting UX** — path tracking is the difference between debug joy and debug despair.
+- **Standards (JSON Schema spec)** — interoperability with thousands of tools.
+- **Library evaluation** — knowing when "build it yourself" beats "pip install" and vice versa.
+- **Code generation** — from schema to sample, from schema to docs, from schema to types.
+
+## Next Steps
+- Add **`additionalProperties: false`** enforcement throughout.
+- Implement **`$ref`** for cross-schema references.
+- Add **`oneOf` / `anyOf`** support for sum types.
+- Compare your output to **`jsonschema`** on the same data.
+- Re-implement using **`pydantic`** classes and feel the speed.
+- Wrap with a **CLI** or **web frontend**.
+
+## Conclusion
+You built a recursive JSON Schema validator that catches type errors, constraint violations, and missing fields with precise error messages. The same shape (data tree + schema tree + recursive walk) underlies every validator in every web framework. From here, switching to `jsonschema` or `pydantic` is incremental — they speak the same language; they just speak it faster and more completely. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/jsondatavalidator.py). Find more data-tooling projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/morsecodetranslator.mdx b/src/content/docs/projects/Beginners/morsecodetranslator.mdx
index cfad7c51..1cb3f237 100644
--- a/src/content/docs/projects/Beginners/morsecodetranslator.mdx
+++ b/src/content/docs/projects/Beginners/morsecodetranslator.mdx
@@ -1,6 +1,6 @@
---
title: Morse Code Translator
-description: A Python application that translates text to Morse code and vice versa, with audio playback functionality for learning and practicing Morse code communication.
+description: Build a Python translator for text ↔ Morse code with audio playback. Learn dictionary inversion, encoding/decoding patterns, cross-platform audio, real Morse timing, and a Tkinter GUI.
sidebar:
order: 25
hero:
@@ -13,275 +13,221 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Morse Code Translator is a Python application that provides bidirectional translation between regular text and Morse code. The application features a comprehensive Morse code dictionary, text-to-Morse conversion, Morse-to-text conversion, and audio playback functionality using system beeps. This project demonstrates dictionary manipulation, string processing, audio programming, and menu-driven interface design. It's an excellent tool for learning Morse code and understanding historical communication methods.
+Morse code was the first global digital encoding — used over telegraph wires, ship-to-shore radio, and emergency signaling for over a century. In this project you build a Python translator that converts text to Morse code and back, plays the code as audio beeps using realistic dot-dash-space timing, and supports the full International Morse alphabet including punctuation. Then we add a Tkinter GUI, cross-platform audio (no more Windows-only `winsound`), visual signaling with screen flashes, and an audio decoder that listens to Morse and transcribes it.
+
+You will learn:
+- The exact mapping between characters and Morse symbols.
+- How to invert a dictionary cleanly for reverse lookup.
+- Realistic Morse timing (the 1:3:1:3:7 rule).
+- Cross-platform audio generation with NumPy and sounddevice.
+- How to encode and decode digital signals — a foundation for any communications project.
## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
-- Windows OS (for winsound module) or alternative audio library
+- Python 3.6 or above.
+- A text editor or IDE.
+- Speakers (for audio playback).
+- Familiarity with dictionaries and string iteration.
+
+## Install Dependencies
+The basic version uses only built-ins. For cross-platform audio:
+```cmd title="install"
+pip install numpy sounddevice
+```
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download).
+## Getting Started
-**Note:** This project uses the `winsound` module which is Windows-specific. For other operating systems, you can modify the audio playback function to use alternative libraries like `pygame` or `playsound`.
+### Create the project
+1. Create folder `morse-code-translator`.
+2. Inside, create `morsecodetranslator.py`.
-## Getting Started
-#### Create a Project
-1. Create a folder named `morse-code-translator`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `morsecodetranslator.py`.
-4. Copy the given code and paste it in your `morsecodetranslator.py` file.
-
-## Write the Code
-1. Copy and paste the following code in your `morsecodetranslator.py` file.
-
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `morse-code-translator`.
-```cmd title="command" showLineNumbers{1} {1-25}
-C:\Users\Your Name\morse-code-translator> python morsecodetranslator.py
-Morse Code Translator
-1. Translate to Morse Code
-2. Translate to Text
-3. Play Morse Code
-4. Exit
-Enter your choice: 1
-Enter the text to translate to Morse Code: HELLO
-Morse Code: .... . .-.. .-.. ---
+### Write the code
+
-Morse Code Translator
+### Run it
+```cmd title="command"
+C:\Users\Your Name\morse-code-translator> python morsecodetranslator.py
1. Translate to Morse Code
2. Translate to Text
3. Play Morse Code
4. Exit
-Enter your choice: 2
-Enter the Morse Code to translate to Text: .... . .-.. .-.. ---
+Choice: 1
Text: HELLO
-
-Morse Code Translator
-1. Translate to Morse Code
-2. Translate to Text
-3. Play Morse Code
-4. Exit
-Enter your choice: 3
-Enter the Morse Code to play: ... --- ...
-# Audio beeps will play the SOS pattern
+Morse: .... . .-.. .-.. ---
```
-## Explanation
-
-### Morse Code Dictionary
-The application uses a comprehensive dictionary mapping characters to Morse code:
-```python title="morsecodetranslator.py" showLineNumbers{1} {1-10}
-morse_code = {
- 'A': '.-', 'B': '-...', 'C': '-.-.', 'D': '-..', 'E': '.',
- 'F': '..-.', 'G': '--.', 'H': '....', 'I': '..', 'J': '.---',
- 'K': '-.-', 'L': '.-..', 'M': '--', 'N': '-.', 'O': '---',
- 'P': '.--.', 'Q': '--.-', 'R': '.-.', 'S': '...', 'T': '-',
- 'U': '..-', 'V': '...-', 'W': '.--', 'X': '-..-', 'Y': '-.--',
- 'Z': '--..', '0': '-----', '1': '.----', '2': '..---', '3': '...--',
- '4': '....-', '5': '.....', '6': '-....', '7': '--...', '8': '---..',
- '9': '----.', ' ': ' ', ',': '--..--', '.': '.-.-.-', '?': '..--..',
- '/': '-..-.', '-': '-....-', '(': '-.--.', ')': '-.--.-'
+## The Morse Code Table
+
+```python title="codes.py"
+MORSE = {
+ "A": ".-", "B": "-...", "C": "-.-.", "D": "-..",
+ "E": ".", "F": "..-.", "G": "--.", "H": "....",
+ "I": "..", "J": ".---", "K": "-.-", "L": ".-..",
+ "M": "--", "N": "-.", "O": "---", "P": ".--.",
+ "Q": "--.-", "R": ".-.", "S": "...", "T": "-",
+ "U": "..-", "V": "...-", "W": ".--", "X": "-..-",
+ "Y": "-.--", "Z": "--..",
+ "0": "-----", "1": ".----", "2": "..---", "3": "...--",
+ "4": "....-", "5": ".....", "6": "-....", "7": "--...",
+ "8": "---..", "9": "----.",
+ ".": ".-.-.-", ",": "--..--", "?": "..--..", "/": "-..-.",
+ "(": "-.--.", ")": "-.--.-", "-": "-....-",
}
+TEXT = {v: k for k, v in MORSE.items()} # reverse lookup, built in one line
```
-
-### Core Functions
-
-#### 1. Text to Morse Code Translation
-```python title="morsecodetranslator.py" showLineNumbers{1} {1-5}
-def translate_to_morse_code(text):
- morse_code_text = ''
- for letter in text:
- morse_code_text += morse_code[letter.upper()] + ' '
- return morse_code_text
+The dictionary-comprehension `{v: k for k, v in MORSE.items()}` is the cleanest way to invert a dict.
+
+## Step-by-Step Explanation
+
+### 1. Text → Morse
+```python title="encode.py"
+def to_morse(text: str) -> str:
+ parts = []
+ for ch in text.upper():
+ if ch == " ":
+ parts.append("/") # word separator
+ elif ch in MORSE:
+ parts.append(MORSE[ch])
+ # silently skip unknown characters
+ return " ".join(parts)
```
-
-#### 2. Morse Code to Text Translation
-```python title="morsecodetranslator.py" showLineNumbers{1} {1-15}
-def translate_to_text(morse_code_text):
- text = ''
- morse_code_text += ' '
- letter = ''
- for symbol in morse_code_text:
- if symbol != ' ':
- i = 0
- letter += symbol
- else:
- i += 1
- if i == 2:
- text += ' '
+- Letters are separated by **one space**.
+- Words are separated by `" / "` (a slash with spaces around it) — the International convention.
+
+### 2. Morse → Text
+```python title="decode.py"
+def from_morse(morse: str) -> str:
+ out = []
+ for word in morse.split(" / "):
+ letters = []
+ for code in word.split():
+ if code in TEXT:
+ letters.append(TEXT[code])
else:
- text += list(morse_code.keys())[list(morse_code.values()).index(letter)]
- letter = ''
- return text
+ letters.append("?") # unknown code
+ out.append("".join(letters))
+ return " ".join(out)
```
-
-#### 3. Audio Playback Function
-```python title="morsecodetranslator.py" showLineNumbers{1} {1-8}
-def play_morse_code(morse_code_text):
- for symbol in morse_code_text:
- if symbol == '.':
- winsound.Beep(1000, 100) # Short beep (dot)
- elif symbol == '-':
- winsound.Beep(1000, 300) # Long beep (dash)
- else:
- time.sleep(0.5) # Pause between letters
+Two split levels: words on `/`, letters on whitespace.
+
+### 3. Audio playback
+The original used Windows-only `winsound`. Here is the cross-platform version using NumPy + sounddevice:
+```python title="play.py"
+import numpy as np
+import sounddevice as sd
+
+UNIT = 0.08 # seconds per "dit" (dot)
+FREQ = 700 # Hz
+
+def tone(seconds):
+ t = np.linspace(0, seconds, int(seconds * 44100), endpoint=False)
+ wave = 0.4 * np.sin(2 * np.pi * FREQ * t)
+ sd.play(wave, 44100); sd.wait()
+
+def silence(seconds):
+ sd.play(np.zeros(int(seconds * 44100)), 44100); sd.wait()
+
+def play_morse(morse: str):
+ for sym in morse:
+ if sym == ".": tone(UNIT)
+ elif sym == "-": tone(UNIT * 3)
+ elif sym == " ": silence(UNIT * 3)
+ elif sym == "/": silence(UNIT * 7)
+ # intra-character gap (between dots/dashes of a letter) = 1 unit
+ silence(UNIT)
+```
+A clean, smooth sine wave instead of harsh beeps.
+
+## Real Morse Timing — The 1:3:1:3:7 Rule
+
+International Morse defines all durations as multiples of one **"unit"**:
+- **Dot (·)**: 1 unit on.
+- **Dash (−)**: 3 units on.
+- **Intra-character gap** (between dots/dashes of the same letter): 1 unit off.
+- **Inter-character gap** (between letters): 3 units off.
+- **Inter-word gap** (between words): 7 units off.
+
+A speed of "20 WPM" (words per minute) means roughly 60 ms per unit. Slow learners use 100 ms; experts use 30 ms.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `KeyError: 'h'` | Forgot to uppercase | `ch = ch.upper()` |
+| Wrong character on decode | Whitespace mismatch (multiple spaces) | `morse.split()` (splits on any whitespace) |
+| Reverse lookup wrong character | Manual inversion left bugs | Use `{v: k for k, v in MORSE.items()}` |
+| Words run together on decode | No `/` separator | Use `" / "` for word breaks |
+| Audio harsh and clicky | Square-wave beeps | Use a sine wave (NumPy) |
+| Only works on Windows | `winsound` | Use `sounddevice` + NumPy |
+
+## Variations to Try
+
+### 1. Visual flasher
+Replace audio with a Tkinter window that flashes:
+```python title="flash.py"
+def flash(seconds, color="white"):
+ label.config(bg=color); root.update(); time.sleep(seconds)
+ label.config(bg="black"); root.update(); time.sleep(UNIT)
```
+Useful for signaling at sea or for accessibility.
-## Features
-- **Bidirectional Translation:** Convert text to Morse code and Morse code back to text
-- **Comprehensive Character Set:** Supports letters, numbers, and common punctuation
-- **Audio Playback:** Listen to Morse code with system beeps
-- **Menu-Driven Interface:** Easy-to-use command-line menu system
-- **Recursive Menu:** Continuous operation until user chooses to exit
-- **Case Insensitive:** Handles both uppercase and lowercase input
-- **Real-time Processing:** Instant translation and playback
-
-## Morse Code Reference
-
-### Letters A-Z
-| Letter | Morse | Letter | Morse | Letter | Morse |
-|--------|-------|--------|-------|--------|-------|
-| A | .- | J | .--- | S | ... |
-| B | -... | K | -.- | T | - |
-| C | -.-. | L | .-.. | U | ..- |
-| D | -.. | M | -- | V | ...- |
-| E | . | N | -. | W | .-- |
-| F | ..- | O | --- | X | -..- |
-| G | --. | P | .--. | Y | -.-- |
-| H | .... | Q | --.- | Z | --.. |
-| I | .. | R | .-. | | |
-
-### Numbers 0-9
-| Number | Morse Code | Number | Morse Code |
-|--------|------------|--------|------------|
-| 0 | ----- | 5 | ..... |
-| 1 | .---- | 6 | -.... |
-| 2 | ..--- | 7 | --... |
-| 3 | ...-- | 8 | ---.. |
-| 4 | ....- | 9 | ----. |
-
-### Common Punctuation
-| Symbol | Morse Code | Symbol | Morse Code |
-|--------|------------|--------|------------|
-| . | .-.-.- | ( | -.--. |
-| , | --..-- | ) | -.--.- |
-| ? | ..--.. | - | -....- |
-| / | -..-. | | |
-
-## Audio Timing Standards
-- **Dot (.):** 100ms beep at 1000Hz
-- **Dash (-):** 300ms beep at 1000Hz
-- **Letter spacing:** 500ms pause
-- **Word spacing:** Represented by spaces in input
-
-## Use Cases
-- **Emergency Communications:** Learn SOS (... --- ...) and other distress signals
-- **Amateur Radio:** Practice for ham radio licensing
-- **Educational:** Teaching communication history and methods
-- **Military/Naval:** Understanding maritime and military communications
-- **Accessibility:** Alternative communication method for specific needs
+### 2. Tkinter GUI
+A window with a text input, output box, and "To Morse" / "To Text" / "Play" / "Stop" buttons. See [Currency Exchange Rate Calculator GUI](./currencyexchnageratecalculator) for the pattern.
-## Next Steps
-You can enhance this project by:
-- Adding a GUI interface with Tkinter or PyQt
-- Implementing adjustable playback speed and frequency
-- Adding visual Morse code display with flashing lights
-- Creating practice modes with random word generation
-- Adding file input/output for batch processing
-- Implementing prosigns (procedural signals) support
-- Adding Morse code keyboard input mode
-- Creating multiplayer Morse code games
-- Adding different audio tones and voices
-- Implementing Morse code over network communication
-
-## Cross-Platform Audio Support
-For non-Windows systems, replace the audio function:
-
-### Using pygame (Cross-platform)
-```python title="morsecodetranslator.py" showLineNumbers{1}
-import pygame
-pygame.mixer.init()
-
-def play_morse_code_pygame(morse_code_text):
- for symbol in morse_code_text:
- if symbol == '.':
- # Generate and play short tone
- play_tone(1000, 100)
- elif symbol == '-':
- # Generate and play long tone
- play_tone(1000, 300)
- else:
- time.sleep(0.5)
-```
+### 3. WAV file export
+Concatenate the audio array and save as `output.wav` with `scipy.io.wavfile.write`.
-### Using playsound (Cross-platform)
-```python title="morsecodetranslator.py" showLineNumbers{1}
-from playsound import playsound
-import tempfile
-import wave
+### 4. Audio decoder
+Use `sounddevice.rec` to capture mic input. Detect peaks above a threshold, measure their duration, and classify as dot/dash. Match gaps to find letter and word boundaries. The hardest part is choosing a sensible threshold — adaptive amplitude detection helps.
-def create_morse_audio(morse_code_text):
- # Generate audio file and play
- pass
-```
+### 5. Adjustable speed
+A `Scale` widget controls WPM. Recalculate `UNIT = 1.2 / wpm` on every change.
-## Educational Applications
-- **Morse Code Learning:** Interactive way to learn the code
-- **Historical Education:** Understanding telegraph communication
-- **STEM Projects:** Combining technology with history
-- **Accessibility Training:** Alternative communication methods
-- **Emergency Preparedness:** Learning distress signals
-
-## Advanced Features Ideas
-```python title="morsecodetranslator.py" showLineNumbers{1}
-def enhanced_morse_translator():
- # Features to implement:
- # - GUI interface with visual indicators
- # - Adjustable speed and pitch
- # - Practice mode with scoring
- # - File batch processing
- # - Network transmission
- # - Visual light patterns
- pass
-
-class MorseCodeTrainer:
- def __init__(self):
- self.difficulty_level = 1
- self.practice_words = []
- self.user_stats = {}
-
- def generate_practice_session(self):
- # Create random practice sessions
- pass
-```
+### 6. Prosigns
+Support `` (end of contact), `` (end of message), `` (new paragraph), `` (named station only).
-## Common Morse Code Phrases
-- **SOS:** ... --- ... (International distress signal)
-- **CQ:** -.-. --.- (General call to any station)
-- **QRT:** --.- .-. - (Stopping transmission)
-- **73:** --... ...-- (Best wishes - amateur radio)
+### 7. Practice mode
+Generate random words; player types what they hear. Score and high-score tracking.
-## Performance Considerations
-- **Memory Usage:** Efficient dictionary lookup operations
-- **Audio Quality:** Consistent timing for clear communication
-- **Response Time:** Quick translation for real-time use
-- **Error Handling:** Graceful handling of invalid input
+### 8. Light-pulse output
+Pair with a Raspberry Pi and an LED — physically blink the Morse code.
-## Educational Value
-This project teaches:
-- **Dictionary Operations:** Key-value mapping and reverse lookup
-- **String Processing:** Character-by-character text manipulation
-- **Audio Programming:** System sound generation and timing
-- **Menu Design:** User interface and navigation logic
-- **Historical Technology:** Understanding communication evolution
+### 9. Network transmission
+Send Morse over a TCP socket between two computers — a working "telegraph network".
+
+### 10. Multi-language
+Cyrillic, Greek, Japanese Wabun all have their own Morse mappings.
+
+## Common Morse Phrases
+
+| Phrase | Morse | Meaning |
+|---|---|---|
+| **SOS** | `... --- ...` | International distress signal (note: not "Save Our Souls" — chosen for being unambiguous) |
+| **CQ** | `-.-. --.-` | "Calling any station" |
+| **73** | `--... ...--` | "Best regards" — ham radio sign-off |
+| **88** | `---.. ---..` | "Love and kisses" |
+| **QTH** | `--.- - ....` | "What is your location?" |
+| **QRT** | `--.- .-. -` | "Stop transmitting" |
## Real-World Applications
-- **Emergency Communication:** Backup communication method
-- **Amateur Radio:** Ham radio operation and practice
-- **Maritime Communication:** Ship-to-shore signaling
-- **Aviation:** Aircraft communication protocols
-- **Military Operations:** Field communication training
+- **Amateur radio (ham)** — Morse is still active on shortwave bands.
+- **Aviation** — VOR/NDB navigation beacons identify themselves in Morse.
+- **Emergency signaling** — light/sound SOS without electronics.
+- **Accessibility** — single-switch input device for people with severe motor disabilities.
+- **Education** — teaching encoding, timing, and digital communication concepts.
+
+## Educational Value
+- **Dictionary inversion** — one of the most common Python idioms.
+- **Encoding & decoding** — generalizes to any symbol-substitution cipher.
+- **Audio generation** — sine waves, sample rates, NumPy basics.
+- **Timing-sensitive code** — what real-time means in practice.
+- **Cross-platform thinking** — Windows vs. Linux vs. macOS audio APIs.
+
+## Next Steps
+- Replace **`winsound`** with the **NumPy + sounddevice** version above.
+- Add a **Tkinter GUI** with text fields and a "play" button.
+- Build an **audio decoder** that listens to Morse and transcribes it.
+- Add **adjustable WPM**.
+- Combine with [Basic Music Player](./basicmusicplayer) for richer audio.
## Conclusion
-In this project, we learned how to create a Morse Code Translator that demonstrates dictionary operations, string processing, and audio programming in Python. The application provides a practical tool for learning and practicing Morse code while showcasing fundamental programming concepts. This project bridges historical communication methods with modern programming techniques, offering both educational value and practical utility. To find more projects like this, you can visit Python Central Hub.
+You built a translator that handles letters, digits, punctuation, and audio playback — the same building blocks every digital communication protocol uses. From the perspective of encoding theory, Morse is the simplest "variable-length prefix code" you can study, and the same principles power UTF-8 and Huffman coding. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/morsecodetranslator.py). Find more encoding projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/movie-recommendation-system.mdx b/src/content/docs/projects/Beginners/movie-recommendation-system.mdx
index 21b72632..8a612135 100644
--- a/src/content/docs/projects/Beginners/movie-recommendation-system.mdx
+++ b/src/content/docs/projects/Beginners/movie-recommendation-system.mdx
@@ -1,6 +1,6 @@
---
title: Movie Recommendation System (Basic)
-description: An intelligent movie recommendation system using collaborative filtering, content-based filtering, and hybrid approaches to suggest personalized movie recommendations
+description: Build a Python recommender with collaborative filtering, content-based filtering, and a hybrid model. Learn Pearson similarity, cold-start handling, matrix factorization, and TMDB API integration.
sidebar:
order: 48
hero:
@@ -13,304 +13,262 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+A recommendation system is a small but real machine-learning project. In ~300 lines you implement two foundational algorithms — **collaborative filtering** ("users like you also enjoyed...") and **content-based filtering** ("because you liked Action, Sci-Fi..."), then blend them in a **hybrid** model. The data is a list of movies with genres and a user-ratings table. The math is Pearson correlation and weighted scoring. The result genuinely surprises you with non-obvious suggestions.
-An intelligent movie recommendation system that provides personalized movie suggestions using multiple recommendation algorithms including collaborative filtering, content-based filtering, and hybrid approaches. This project demonstrates machine learning concepts, data analysis, and recommendation algorithms.
+You will learn:
+- The two foundational recommendation paradigms and their trade-offs.
+- How Pearson correlation measures rating-pattern similarity.
+- Why content-based filtering solves the cold-start problem.
+- How to blend recommenders for robustness.
+- The path from this toy to production-grade matrix factorization (SVD, ALS).
-## 🎯 Project Overview
+## Prerequisites
+- Python 3.7 or above (for `dataclasses`).
+- A text editor or IDE.
+- Comfort with classes, dictionaries, and basic statistics.
+- Familiarity with [Personal Diary](./personaldiary) for the JSON-persistence pattern.
-This project implements a comprehensive movie recommendation system featuring:
-- User registration and rating system
-- Multiple recommendation algorithms
-- Movie database with genres and ratings
-- Statistical analysis and insights
-- Search and filtering capabilities
-- Data persistence using JSON storage
+## Getting Started
-## ✨ Features
+### Create the project
+1. Create folder `movie-recommender`.
+2. Inside, create `movierecommendationsystem.py`.
-### Recommendation Algorithms
-- **Collaborative Filtering**: Recommendations based on similar users' preferences
-- **Content-Based Filtering**: Suggestions based on movie genres and user preferences
-- **Hybrid Approach**: Combines both methods for better accuracy
-- **Pearson Correlation**: Measures user similarity for collaborative filtering
+### Write the code
+
-### User Management
-- **User Registration**: Create and manage user profiles
-- **Rating System**: Rate movies on a 1-5 scale
-- **Preference Learning**: Automatically discover favorite genres
-- **Rating History**: Track all user ratings and preferences
-
-### Movie Database
-- **Comprehensive Catalog**: Pre-loaded with popular movies
-- **Genre Classification**: Multi-genre support for accurate categorization
-- **Rating Analytics**: Average ratings and rating counts
-- **Search Functionality**: Find movies by title or genre
-
-### Analytics & Insights
-- **Top Rated Movies**: Discover highest-rated films
-- **Statistical Dashboard**: Database statistics and trends
-- **Genre Analysis**: Most popular genres and distributions
-- **User Profiles**: Personal rating history and preferences
-
-## 🛠️ Technical Implementation
-
-### Class Structure
-```python title="movierecommendationsystem.py" showLineNumbers{1}
-class Movie:
- def __init__(self, movie_id, title, genres, year=None):
- # Movie metadata and rating management
- # Automatic rating calculations
-
- def add_rating(self, rating):
- # Add user rating and update averages
-
- def to_dict(self):
- # Serialize movie data for storage
-
-class User:
- def __init__(self, user_id, name):
- # User profile and rating management
- # Favorite genre tracking
-
- def rate_movie(self, movie_id, rating):
- # Rate a movie and update preferences
-
- def update_favorite_genres(self, movies):
- # Analyze ratings to determine favorite genres
-
-class MovieRecommendationSystem:
- def __init__(self, data_file="movie_data.json"):
- # Initialize system with data persistence
- # Load existing data or create sample data
-```
-
-### Key Algorithms
-
-#### Collaborative Filtering
-```python title="movierecommendationsystem.py" showLineNumbers{1}
-def calculate_user_similarity(self, user1_id, user2_id):
- # Calculate Pearson correlation coefficient
- # Find users with similar rating patterns
- # Return similarity score between -1 and 1
-
-def get_user_based_recommendations(self, user_id, limit=10):
- # Find similar users
- # Aggregate recommendations from similar users
- # Weight recommendations by user similarity
- # Return top-rated movies from similar users
+### Run it
+```bash title="run"
+python movierecommendationsystem.py
```
-#### Content-Based Filtering
-```python title="movierecommendationsystem.py" showLineNumbers{1}
-def get_content_based_recommendations(self, user_id, limit=10):
- # Analyze user's favorite genres
- # Score unrated movies based on genre preferences
- # Consider movie ratings and popularity
- # Return movies matching user's taste profile
```
-
-#### Hybrid Approach
-```python title="movierecommendationsystem.py" showLineNumbers{1}
-def get_hybrid_recommendations(self, user_id, limit=10):
- # Combine collaborative and content-based recommendations
- # Apply weighted scoring (70% collaborative, 30% content-based)
- # Merge and rank final recommendations
- # Return diverse, personalized suggestions
-```
-
-## 🚀 How to Run
-
-1. **Install Python**: Ensure Python 3.7+ is installed
-2. **Run the System**:
- ```bash
- python movierecommendationsystem.py
- ```
-
-3. **Getting Started**:
- - Register as a new user or login with existing ID
- - Browse and rate some movies to build your profile
- - Get personalized recommendations
- - Explore different recommendation types
-
-## 💡 Usage Examples
-
-### Basic Usage
-```python title="movierecommendationsystem.py" showLineNumbers{1}
-# Create recommendation system
-system = MovieRecommendationSystem()
-
-# Add a new user
-user = system.add_user("John Doe")
-
-# Rate some movies
-system.rate_movie(user.id, 1, 5.0) # Love "The Shawshank Redemption"
-system.rate_movie(user.id, 3, 4.5) # Really like "The Dark Knight"
-system.rate_movie(user.id, 6, 4.0) # Enjoy "Inception"
-
-# Get recommendations
-recommendations = system.get_hybrid_recommendations(user.id, 5)
-for movie in recommendations:
- print(f"Recommended: {movie}")
+1. Register user
+2. Rate movie
+3. Get recommendations
+4. Browse / search
+5. Statistics
+6. Quit
+> 3
+User ID: 1
+Method: (1) collaborative (2) content (3) hybrid: 3
+1. The Dark Knight (4.8 expected)
+2. Pulp Fiction (4.6 expected)
+3. The Matrix (4.5 expected)
```
-### Advanced Features
-```python title="movierecommendationsystem.py" showLineNumbers{1}
-# Find similar users
-similarity = system.calculate_user_similarity(user1_id, user2_id)
-print(f"User similarity: {similarity:.2f}")
+## Step-by-Step Explanation
-# Get genre-specific movies
-action_movies = system.get_movies_by_genre("Action")
+### 1. Data models
+```python title="models.py"
+@dataclass
+class Movie:
+ id: int
+ title: str
+ genres: list[str]
+ year: int | None = None
+ ratings: list[float] = field(default_factory=list)
-# Search for movies
-search_results = system.search_movies("dark knight")
+ @property
+ def avg(self) -> float:
+ return sum(self.ratings) / len(self.ratings) if self.ratings else 0.0
-# Get top-rated movies
-top_movies = system.get_top_rated_movies(10, min_ratings=5)
+@dataclass
+class User:
+ id: int
+ name: str
+ ratings: dict[int, float] = field(default_factory=dict) # {movie_id: rating}
+
+ @property
+ def favorite_genres(self) -> list[str]:
+ scores = {}
+ for mid, r in self.ratings.items():
+ if r >= 4:
+ for g in movie_by_id[mid].genres:
+ scores[g] = scores.get(g, 0) + 1
+ return sorted(scores, key=scores.get, reverse=True)
```
-
-### Analytics
-```python title="movierecommendationsystem.py" showLineNumbers{1}
-# Get comprehensive statistics
-stats = system.get_movie_statistics()
-print(f"Total movies: {stats['total_movies']}")
-print(f"Most popular genres: {stats['most_common_genres']}")
+`@property` lets `user.favorite_genres` look like an attribute but compute fresh from current ratings every time.
+
+### 2. Pearson similarity
+```python title="pearson.py"
+import math
+
+def pearson(u: User, v: User) -> float:
+ common = set(u.ratings) & set(v.ratings)
+ if len(common) < 2: return 0.0 # too little overlap
+ n = len(common)
+ sum_u = sum(u.ratings[m] for m in common)
+ sum_v = sum(v.ratings[m] for m in common)
+ sq_u = sum(u.ratings[m] ** 2 for m in common)
+ sq_v = sum(v.ratings[m] ** 2 for m in common)
+ cross = sum(u.ratings[m] * v.ratings[m] for m in common)
+
+ num = cross - (sum_u * sum_v / n)
+ den = math.sqrt((sq_u - sum_u**2/n) * (sq_v - sum_v**2/n))
+ return num / den if den else 0.0
```
-
-## 🎨 Interactive Features
-
-### Main Menu System
-1. **Register/Login User** - User account management
-2. **Browse Movies** - View all movies or filter by genre
-3. **Search Movies** - Find movies by title
-4. **Rate Movie** - Add ratings to build your profile
-5. **Get Recommendations** - Choose from different recommendation types
-6. **View Top Rated Movies** - Discover highly-rated films
-7. **Add New Movie** - Expand the movie database
-8. **View Statistics** - Database analytics and insights
-9. **User Profile** - View your ratings and preferences
-
-### Recommendation Types
-- **User-based**: "Users like you also enjoyed..."
-- **Content-based**: "Because you liked [Genre]..."
-- **Hybrid**: "Personalized for you"
-
-## 📊 Sample Data
-
-The system comes pre-loaded with 20 popular movies including:
-- The Shawshank Redemption (1994) - Drama, Crime
-- The Dark Knight (2008) - Action, Crime, Drama
-- Inception (2010) - Action, Sci-Fi, Thriller
-- Pulp Fiction (1994) - Crime, Drama
-- The Matrix (1999) - Action, Sci-Fi
-
-Each movie includes:
-- Multiple genres for accurate categorization
-- Release year for temporal analysis
-- Pre-generated ratings for immediate functionality
-
-## 🔧 Advanced Features
-
-### Similarity Calculation
-```python title="movierecommendationsystem.py" showLineNumbers{1}
-# Pearson correlation coefficient for user similarity
-def calculate_user_similarity(self, user1_id, user2_id):
- # Find movies rated by both users
- # Calculate correlation between rating patterns
- # Return similarity score (-1 to 1)
+Pearson correlation is the standard similarity metric for collaborative filtering. It is **bias-aware** — two users with the same shape of opinions but different "harshness" still correlate strongly.
+
+### 3. Collaborative filtering
+```python title="collab.py"
+def collaborative(user: User, movies: dict, users: dict, k: int = 10):
+ scores = {}
+ for other_id, other in users.items():
+ if other_id == user.id: continue
+ sim = pearson(user, other)
+ if sim <= 0: continue
+ for mid, rating in other.ratings.items():
+ if mid in user.ratings: continue # skip movies user already rated
+ scores.setdefault(mid, [0.0, 0.0])
+ scores[mid][0] += sim * rating # weighted sum
+ scores[mid][1] += sim # normalization factor
+ ranked = sorted(
+ ((mid, num/den) for mid, (num, den) in scores.items() if den > 0),
+ key=lambda x: x[1], reverse=True)
+ return [movies[mid] for mid, _ in ranked[:k]]
```
-
-### Preference Learning
-```python title="movierecommendationsystem.py" showLineNumbers{1}
-# Automatic genre preference detection
-def update_favorite_genres(self, movies):
- # Analyze highly-rated movies (4.0+)
- # Calculate average rating per genre
- # Rank genres by preference strength
+Score = Σ(similarity × their_rating) / Σ similarity. The division produces a number on the rating scale, not just a ranking.
+
+### 4. Content-based filtering
+```python title="content.py"
+def content_based(user: User, movies: dict, k: int = 10):
+ if not user.favorite_genres:
+ return top_rated(movies, k) # cold-start fallback
+ scores = {}
+ fav = set(user.favorite_genres[:3])
+ for mid, m in movies.items():
+ if mid in user.ratings: continue
+ overlap = len(set(m.genres) & fav)
+ if overlap == 0: continue
+ scores[mid] = overlap * (1 + m.avg) # boost well-rated matches
+ return [movies[mid] for mid, _ in
+ sorted(scores.items(), key=lambda x: x[1], reverse=True)[:k]]
```
-
-### Weighted Recommendations
-```python title="movierecommendationsystem.py" showLineNumbers{1}
-# Hybrid scoring system
-score = (collaborative_score * 0.7) + (content_score * 0.3)
+Score = (genre overlap) × (1 + average rating). New users with no ratings get the top-rated list as a fallback — the **cold-start** solution.
+
+### 5. Hybrid
+```python title="hybrid.py"
+def hybrid(user, movies, users, k=10):
+ collab = collaborative(user, movies, users, k * 2)
+ content = content_based(user, movies, k * 2)
+ score = {}
+ for i, m in enumerate(collab): score[m.id] = score.get(m.id, 0) + 0.7 * (len(collab) - i)
+ for i, m in enumerate(content): score[m.id] = score.get(m.id, 0) + 0.3 * (len(content) - i)
+ return [movies[mid] for mid, _ in
+ sorted(score.items(), key=lambda x: x[1], reverse=True)[:k]]
```
-
-## 📈 Data Structure
-
-### Movie Format
-```json
-{
- "id": 1,
- "title": "The Shawshank Redemption",
- "genres": ["Drama", "Crime"],
- "year": 1994,
- "ratings": [5.0, 4.5, 5.0, 4.0],
- "average_rating": 4.6,
- "rating_count": 4
-}
-```
-
-### User Format
-```json
-{
- "id": 1,
- "name": "John Doe",
- "ratings": {"1": 5.0, "3": 4.5, "6": 4.0},
- "favorite_genres": ["Drama", "Action", "Sci-Fi"]
-}
+70 % weight on collaborative, 30 % on content — a typical starting point. Tune empirically.
+
+## The Cold-Start Problem
+
+When a new user joins, collaborative filtering has nothing to work with. Three standard fixes:
+- **Content-based fallback** for the first few ratings.
+- **Onboarding survey** — ask "pick three favorite genres."
+- **Popularity baseline** — show top-rated movies first.
+
+When a new movie is added, the same problem appears from the other side. Content-based handles this fine because the algorithm only needs the new movie's *genres*, not its ratings.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| All recommendations are the same | Tiny rating dataset | Add minimum-overlap threshold, fall back to popularity |
+| Pearson returns NaN | Identical ratings (variance zero) | Guard with `if den == 0` |
+| Recommended movies user already rated | Forgot to filter | Skip `mid in user.ratings` |
+| Bias toward heavy raters | No normalization | Mean-center each user's ratings before similarity |
+| Score scale meaningless | Used Σ instead of weighted average | Divide by Σ similarity |
+| Hybrid no better than collaborative | Wrong weights | Cross-validate on held-out ratings |
+
+## Variations to Try
+
+### 1. Mean-centered Pearson
+Subtract each user's average rating before computing similarity — removes "harsh rater" bias entirely.
+
+### 2. Cosine similarity
+Cheaper than Pearson:
+```python title="cosine.py"
+def cosine(u, v):
+ common = set(u.ratings) & set(v.ratings)
+ if not common: return 0
+ dot = sum(u.ratings[m] * v.ratings[m] for m in common)
+ nu = math.sqrt(sum(u.ratings[m]**2 for m in common))
+ nv = math.sqrt(sum(v.ratings[m]**2 for m in common))
+ return dot / (nu * nv) if nu * nv else 0
```
-## 🛡️ Error Handling
-
-- **Data Validation**: Validate ratings (1.0-5.0 range)
-- **File Operations**: Handle JSON file errors gracefully
-- **User Input**: Validate all user inputs and provide feedback
-- **Missing Data**: Handle cases with insufficient data for recommendations
-
-## 📚 Learning Objectives
+### 3. Item-based collaborative filtering
+Find movies that are *similar to each other* based on rating patterns. Recommend movies similar to the user's highly-rated ones. Often outperforms user-based at scale.
-- **Recommendation Systems**: Understanding different recommendation approaches
-- **Machine Learning Concepts**: Collaborative and content-based filtering
-- **Data Analysis**: Statistical analysis and correlation calculation
-- **User Interface Design**: Interactive command-line applications
-- **Data Persistence**: JSON storage and data management
+### 4. TF-IDF on genres + descriptions
+Pull movie plot summaries from **TMDB** or **OMDb**. Run TF-IDF on the text and use cosine similarity over the resulting vectors. Far richer content-based recommendations.
-## 🎯 Algorithm Insights
-
-### Collaborative Filtering Strengths:
-- Discovers unexpected connections
-- Works well with sufficient user data
-- Finds niche preferences
-
-### Content-Based Filtering Strengths:
-- Works for new users with few ratings
-- Transparent reasoning for recommendations
-- Consistent with user preferences
-
-### Hybrid Approach Benefits:
-- Combines strengths of both methods
-- More robust recommendations
-- Better coverage of different scenarios
-
-## 🚀 Potential Enhancements
-
-1. **Machine Learning**: Implement matrix factorization algorithms
-2. **Web Interface**: Create Flask/Django web application
-3. **Database Integration**: Use PostgreSQL or MongoDB
-4. **Real-time Updates**: Live recommendation updates
-5. **Social Features**: Friend recommendations and reviews
-6. **Advanced Analytics**: Recommendation effectiveness metrics
-7. **External APIs**: Integration with TMDB or IMDB
-8. **Deep Learning**: Neural collaborative filtering
-
-## 🏆 Project Completion
-
-This Movie Recommendation System demonstrates:
-- ✅ Multiple recommendation algorithms implementation
-- ✅ User profiling and preference learning
-- ✅ Data persistence and management
-- ✅ Statistical analysis and insights
-- ✅ Interactive user interface
-- ✅ Comprehensive error handling
-
-Perfect for beginners learning recommendation systems and intermediate developers exploring machine learning concepts!
+### 5. Matrix factorization (SVD)
+Decompose the user-item rating matrix into low-rank factors. Use `scipy.sparse.linalg.svds` or the `Surprise` library:
+```bash title="install"
+pip install scikit-surprise
+```
+```python title="surprise.py"
+from surprise import Dataset, Reader, SVD
+algo = SVD()
+algo.fit(Dataset.load_from_df(ratings_df, Reader(rating_scale=(1, 5))).build_full_trainset())
+predicted = algo.predict(user_id, movie_id).est
+```
+This is what Netflix used in their famous prize-winning recommender.
+
+### 6. Implicit feedback
+Switch from explicit ratings (1-5 stars) to implicit (watched / not watched). Different math (ALS instead of SVD) but same shape.
+
+### 7. Diversity & serendipity
+After ranking, post-process to ensure variety — penalize candidates whose genres overlap heavily with already-shown items.
+
+### 8. TMDB API integration
+Replace the hard-coded 20 movies with a fresh feed of currently-popular titles from [themoviedb.org/documentation/api](https://www.themoviedb.org/documentation/api).
+
+### 9. Save & evaluate
+Hold out 20 % of ratings; measure RMSE on predicted vs. actual. The standard offline-evaluation pattern.
+
+### 10. Web frontend
+Flask app that renders the user's recommendations with poster art from TMDB. See [Basic Web Server](./basicwebserver).
+
+### 11. Tag-based filtering
+User-added tags ("psychological thriller", "based on a true story") often outperform official genres. Combine.
+
+### 12. Real-time updates
+Re-run recommendations every time a new rating arrives, not just on demand.
+
+## Evaluation Metrics
+
+For any non-trivial recommender, measure:
+- **RMSE** — square root of mean squared prediction error.
+- **MAE** — mean absolute error.
+- **Precision@K** — fraction of top-K recommendations that the user actually liked.
+- **Recall@K** — fraction of liked items that appeared in top-K.
+- **Coverage** — fraction of catalog that ever gets recommended.
+- **Serendipity** — non-obvious good recommendations.
+
+## Real-World Recommender Systems
+- **Netflix** — matrix factorization + deep learning, billions of dollars of ROI.
+- **YouTube** — two-stage: candidate generation → ranking, both neural.
+- **Spotify** — collaborative filtering + audio embeddings.
+- **Amazon** — item-based collaborative filtering ("customers who bought this...").
+- **TikTok** — embedding-based with continuous learning from watch time.
+- **Goodreads / Letterboxd** — explicit ratings, classic CF.
+
+## Educational Value
+- **Collaborative vs. content-based** — two paradigms, different strengths.
+- **Pearson correlation** — a textbook statistic with real predictive power.
+- **Cold-start strategies** — universal recommender system problem.
+- **Hybrid models** — when combining two mediocre models beats either alone.
+- **Evaluation methodology** — train/test splits, held-out ratings.
+
+## Next Steps
+- Implement **item-based** collaborative filtering and compare to user-based.
+- Add **TMDB integration** for live, plot-aware data.
+- Switch to **SVD via `Surprise`** for a real model.
+- Evaluate **RMSE on held-out ratings** to pick the best algorithm.
+- Wrap with a **Flask UI** that renders posters.
+- Cross-link with [Personal Diary](./personaldiary) for the JSON-persistence pattern.
+
+## Conclusion
+You implemented two of the foundational recommendation algorithms and combined them — the same architecture used by the first decade of commercial recommenders. The state of the art has moved on to deep learning, but every modern system still rests on the same two ideas: "users like this one" and "items like this one." Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/movierecommendationsystem.py). Find more ML projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/numberguessingwithai.mdx b/src/content/docs/projects/Beginners/numberguessingwithai.mdx
index 93e37ecd..99c72258 100644
--- a/src/content/docs/projects/Beginners/numberguessingwithai.mdx
+++ b/src/content/docs/projects/Beginners/numberguessingwithai.mdx
@@ -1,6 +1,6 @@
---
title: Number Guessing Game with AI
-description: Play a number guessing game against an AI that uses binary search, then try to beat its score
+description: Build a two-round number guessing game where an AI uses binary search to guess your number, then you try to beat it. Learn algorithms, complexity, and how to think like a computer.
sidebar:
order: 50
hero:
@@ -14,31 +14,60 @@ import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Challenge yourself against an AI in a number guessing game! The AI uses binary search to guess your number, then you try to guess the AI's number. Compare rounds to see who wins and learn about efficient algorithms in action.
+This project takes the classic "Guess the Number" game and flips it on its head: first the computer guesses *your* number, then you guess the computer's number, and whoever needs fewer rounds wins. The interesting part is that the computer does not guess randomly — it uses **binary search**, an algorithm that guarantees it can find any integer between 0 and 100 in at most seven guesses. Watching it work is one of the best practical introductions to algorithmic thinking.
+
+You will learn:
+- How **binary search** works step-by-step.
+- Why algorithm choice matters: random guessing might take 100 tries, binary search takes seven.
+- The concept of **logarithmic time complexity** (`O(log n)`).
+- How to structure a two-phase game with score comparison.
+- How to extend the "AI" with more sophisticated strategies.
+
+## Why Binary Search Is Magic
+
+Imagine you are looking for a word in a dictionary. You do not start at "A" and read every page. You open it in the middle. If your word comes later in the alphabet, you ignore the first half and repeat with the second half. Each step halves the problem.
+
+That is exactly what the AI does:
+
+| Round | Range | Mid | Your answer | New range |
+|---|---|---|---|---|
+| 1 | 0–100 | **50** | "lower" | 0–49 |
+| 2 | 0–49 | **25** | "higher" | 26–49 |
+| 3 | 26–49 | **37** | "lower" | 26–36 |
+| 4 | 26–36 | **31** | "higher" | 32–36 |
+| 5 | 32–36 | **34** | "yes" | done |
+
+In **5 guesses** out of 101 possible numbers. Random guessing would have averaged about 50 attempts.
+
+Mathematically, binary search uses about `log₂(n)` guesses for `n` numbers. `log₂(101) ≈ 6.66`, so at most 7 — always.
## Prerequisites
-- Python 3.6 or above
-- Text Editor or IDE
-- Basic understanding of Python syntax
-- Understanding of loops and input/output
-- Familiarity with binary search (helpful)
+- Python 3.6 or above.
+- A text editor or IDE.
+- Comfort running scripts and reading basic Python.
+- Familiarity with conditionals and loops.
+
+## Concepts You Will Use
+
+- **`random.randint(a, b)`** — pick a random integer for the human-to-guess phase.
+- **`while` loop with a shrinking range** — the core of binary search.
+- **Integer division `//`** — `(low + high) // 2` gives the midpoint without floats.
+- **Input validation** — making sure the player's hint is `h`, `l`, or `y`.
+- **Counter variables** — track how many rounds each side took.
## Getting Started
-### Create a new project
-1. Create a new project folder and name it `numberGuessingWithAI`.
-2. Create a new file and name it `numberguessingwithai.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into your `numberguessingwithai.py` file.
+### Create the project
+1. Create a folder named `number-guessing-ai`.
+2. Inside it, create `numberguessingwithai.py`.
### Write the code
-1. Add the following code to your `numberguessingwithai.py` file.
-2. Save the file.
-3. Run the following command to run the application.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\numberGuessingWithAI> python numberguessingwithai.py
+
+### Run it
+```cmd title="command"
+C:\Users\username\Documents\number-guessing-ai> python numberguessingwithai.py
Think of a number between 0 and 100!
Is your number 50? (h/l/y): h
Is your number 75? (h/l/y): l
@@ -53,32 +82,175 @@ Correct! You guessed my number in 3 rounds!
You win this round!
```
-## Explanation
+## Step-by-Step Explanation
+
+### 1. Set up randomness
+```python title="numberguessingwithai.py"
+import random
+```
+Used for the second phase, where you have to guess a random number the computer generates.
+
+### 2. The AI's binary-search loop
+```python title="numberguessingwithai.py"
+low, high = 0, 100
+ai_rounds = 0
+while True:
+ ai_rounds += 1
+ guess = (low + high) // 2
+ response = input(f"Is your number {guess}? (h/l/y): ").lower()
+ if response == "y":
+ break
+ elif response == "h": # actual number is higher
+ low = guess + 1
+ elif response == "l": # actual number is lower
+ high = guess - 1
+ else:
+ print("Please answer h, l, or y.")
+ ai_rounds -= 1 # do not count this round
+print(f"I guessed your number in {ai_rounds} rounds!")
+```
+
+How this works:
+- `(low + high) // 2` is the midpoint. `//` is integer division, so the result is always a whole number.
+- If the player says "higher", the answer must be above `guess`, so we set `low = guess + 1`. We use `+ 1` because `guess` itself was just ruled out.
+- If "lower", we set `high = guess - 1` for the same reason.
+- If "yes", we win and `break`.
+- The unknown-input branch undoes the round counter to avoid penalizing the AI for the player's typo.
+
+### 3. The human's turn
+```python title="numberguessingwithai.py"
+secret = random.randint(0, 100)
+human_rounds = 0
+while True:
+ human_rounds += 1
+ try:
+ guess = int(input("Your guess: "))
+ except ValueError:
+ print("Please enter a whole number.")
+ human_rounds -= 1
+ continue
+ if guess == secret:
+ print(f"Correct! You guessed my number in {human_rounds} rounds!")
+ break
+ print("Too low!" if guess < secret else "Too high!")
+```
+- `random.randint(0, 100)` picks the secret number once, before the loop.
+- `int(input(...))` is wrapped in `try/except` so letters or empty input do not crash the game.
+- A ternary expression (`"Too low!" if … else "Too high!"`) avoids the need for a full `if/else` block on that line.
+
+### 4. Pick the winner
+```python title="numberguessingwithai.py"
+if ai_rounds < human_rounds:
+ print("AI wins — it guessed faster.")
+elif human_rounds < ai_rounds:
+ print("You win!")
+else:
+ print("Tie!")
+```
+
+## Complexity Notes
+
+Binary search has time complexity **O(log n)**. For range `n`:
+
+| n | Max rounds (`⌈log₂ n⌉ + 1`) |
+|---|---|
+| 10 | 4 |
+| 100 | 7 |
+| 1,000 | 10 |
+| 1,000,000 | 20 |
+| 1,000,000,000 | 30 |
+
+Doubling the range adds **one** extra round. That is why binary search underlies sorted-collection lookups, version-control bisect, and many machine-learning hyperparameter searches.
+
+Random guessing would be **O(n)** — proportional to the size of the range. Even 1,000 numbers gets painful.
+
+## Robust Input
+
+In the AI phase, an unfair player might say "higher" when their number is actually lower. The AI cannot detect that — the bug shows up as an empty range:
+
+```python title="catch_cheat.py"
+if low > high:
+ print("Hmm, your answers don't add up. Did you switch your number?")
+ break
+```
+
+Add that check inside the AI loop and the game ends gracefully when the search collapses.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| AI keeps guessing the same number | Forgot `+ 1` / `- 1` when shrinking the range | `low = guess + 1` and `high = guess - 1` |
+| Range crashes after many rounds | Player gave conflicting hints | Detect `low > high` and exit politely |
+| AI guesses outside 0–100 | Off-by-one in initial range | Initialize `low, high = 0, 100`, not `1, 99` |
+| Tie always favors AI | You used `<` not `<=` | Use a separate `elif` for the tie case |
+
+## Variations to Try
+
+### 1. Adjustable range
+Ask the player for the range at the start:
+```python title="custom_range.py"
+low_input = int(input("Lower bound: "))
+high_input = int(input("Upper bound: "))
+```
+
+### 2. Best-of-three rounds
+Wrap the whole game in a loop that tracks total wins:
+```python title="series.py"
+ai_wins = human_wins = 0
+for round_num in range(1, 4):
+ play_one_round()
+ # tally
+print(f"Series winner: {'AI' if ai_wins > human_wins else 'You'}")
+```
+
+### 3. Smarter "AI" strategies
+- **Random AI** — guess uniformly at random; great baseline.
+- **Pattern AI** — bias toward numbers humans pick often (lots of 7s and 17s).
+- **Adaptive AI** — track which numbers the player has used in past games and avoid them.
+
+### 4. Hide the binary search behind a "thinking" delay
+```python title="thinking.py"
+import time
+print("Thinking...", end="", flush=True); time.sleep(0.5)
+```
+Adds personality without slowing gameplay.
+
+### 5. Add a GUI
+Tkinter with two number-entry fields and a big "Guess!" button. Display the AI's current range visually.
+
+### 6. Compare algorithms head-to-head
+Run binary search vs. random guessing 10,000 times each, average the rounds, and graph the result with `matplotlib`.
+
+### 7. Multiplayer scoreboard
+Persist round counts in a JSON file. Each session you can see your improvement vs. the AI.
+
+## Real-World Algorithm Examples
+
+Binary search shows up everywhere:
+- **`bisect`** module in Python's standard library — insert into a sorted list in O(log n).
+- **`git bisect`** — finds which commit introduced a bug by binary-searching commits.
+- **Database B-tree indexes** — searches narrow the range repeatedly.
+- **Auto-tuning compilers** — find the largest input size that still fits a time budget.
+- **Numerical methods** — bisection for root-finding (e.g., solving `f(x) = 0`).
+
+## Educational Value
-1. The `import random` statement imports the random module for generating random numbers.
-2. The AI uses binary search algorithm to efficiently guess your number.
-3. The `low` and `high` variables maintain the search range for the AI.
-4. The AI starts by guessing the middle value (50) and adjusts based on your response.
-5. Your responses ('h' for higher, 'l' for lower, 'y' for yes) guide the AI's search.
-6. The `random.randint(0, 100)` generates a random number for you to guess.
-7. The program tracks rounds taken by both AI and human player.
-8. Winner determination compares the number of rounds each player needed.
-9. The binary search algorithm guarantees the AI will find any number in at most 7 rounds.
-10. The game demonstrates the efficiency of algorithmic problem-solving vs. human intuition.
+This project teaches:
+- **Algorithmic thinking** — pick a strategy, prove an upper bound, implement it.
+- **Trade-offs** — the AI is fast and fair; a random guesser is simpler but slower.
+- **Logarithmic growth** — intuition for one of the most important concepts in computer science.
+- **State management** — `low` and `high` together represent the AI's belief about the answer.
+- **Robust input handling** — protecting against bad data and inconsistent answers.
## Next Steps
-Congratulations! You have successfully created a Number Guessing Game with AI in Python. Experiment with the code and see if you can modify the application. Here are a few suggestions:
-- Add difficulty levels (change range from 0-100 to larger ranges)
-- Track high scores and statistics across multiple games
-- Add GUI with Tkinter for better user experience
-- Let AI learn from previous games to improve performance
-- Implement different AI strategies (random, pattern-based)
-- Add multiplayer support for multiple human players
-- Create tournament mode with best-of-series gameplay
-- Add sound effects and visual feedback
-- Implement hints system for human player
+- Implement a **graphical version** with a number line that shrinks each round.
+- Build a **command-line tournament**: a dozen different AI strategies competing.
+- Try the **20 Questions** variant: yes/no questions over a database of objects.
+- Read about **interpolation search** — even faster when data is uniformly distributed.
+- Move on to the [Hangman](./hangman) project for a different style of search.
## Conclusion
-In this project, you learned how to create a Number Guessing Game with AI in Python. You also learned about binary search algorithms, human vs. computer problem-solving, and competitive programming concepts. You can find the source code on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/numberguessingwithai.py)
+You implemented binary search from scratch, paired it with a human-vs-computer game loop, and saw why one good algorithm beats brute force every time. The same idea — halve the problem at each step — drives most of the fast lookup code you will ever touch. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/numberguessingwithai.py). More beginner projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/paint.mdx b/src/content/docs/projects/Beginners/paint.mdx
index af6f9d9e..4c3eff70 100644
--- a/src/content/docs/projects/Beginners/paint.mdx
+++ b/src/content/docs/projects/Beginners/paint.mdx
@@ -1,6 +1,6 @@
---
title: Basic Paint Application
-description: Create a digital painting application with color selection, brush tools, and save functionality using Tkinter and PIL
+description: Build a desktop paint program in Python with Tkinter and Pillow. Learn canvas drawing, mouse events, color pickers, brush controls, save-as-image, and how to grow a sketch tool into a full-featured editor.
sidebar:
order: 48
hero:
@@ -14,43 +14,49 @@ import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Build a feature-rich digital painting application with a graphical interface that includes color palettes, brush size controls, eraser tools, canvas background customization, and image saving capabilities. This project demonstrates advanced GUI development, event handling, and image processing.
+A paint program is the perfect mid-level project because it stretches every part of GUI programming: drawing on a canvas, tracking mouse motion, handling many simultaneous widgets, and saving the result to a file. In this project you will build a Tkinter-based paint application with a color palette, an adjustable brush, an eraser, a canvas-color picker, a clear button, and a save-to-JPEG feature.
-## Prerequisites
-
-- Basic understanding of Python syntax
-- Knowledge of Tkinter for GUI development
-- Familiarity with event-driven programming
-- Understanding of coordinate systems and graphics
-- Basic knowledge of image processing concepts
+Along the way you will learn:
+- How to capture continuous mouse-drag events with Tkinter bindings.
+- Why drawing a stream of small ovals looks like a smooth brush stroke.
+- How to lay out widgets with frames, grids, and packs.
+- How to capture the canvas as an image via screen grab.
+- How to organize a non-trivial GUI program with a class.
-## Getting Started
+## Prerequisites
+- Python 3.6 or above.
+- A text editor or IDE.
+- Comfort with classes (or a willingness to learn — we use them here).
+- Familiarity with Tkinter basics. If you have built the [Basic Music Player](./basicmusicplayer) you already know enough.
+## Install Pillow
-1. **Install Required Dependencies**
- ```bash
- pip install pillow
- # tkinter comes built-in with Python
- ```
+The save-as-image feature uses **Pillow** (the fork of the classic PIL):
+```bash title="install"
+pip install pillow
+```
+Tkinter itself ships with Python — no install.
-2. **Run the Paint Application**
- ```bash
- python paint.py
- ```
+## Getting Started
-3. **Start Creating Art**
- - Select colors from the color palette
- - Adjust brush size with the scale control
- - Use the eraser tool for corrections
- - Save your artwork when finished
+### Create the project
+1. Create a folder named `paint-app`.
+2. Inside it, create `paint.py`.
+### Write the code
+
+### Run it
+```bash title="run"
+python paint.py
+```
+A window appears with a color palette on the left, a brush-size slider on the right, a clear button at the top, and a large white canvas in the middle. Click and drag to draw.
-## Code Explanation
+## Step-by-Step Explanation
-### Application Architecture
-```python title="paint.py" showLineNumbers{1}
+### 1. Class skeleton
+```python title="paint.py"
class Paint:
def __init__(self, root):
self.root = root
@@ -58,107 +64,172 @@ class Paint:
self.root.geometry("800x520")
self.pen_color = "black"
self.eraser_color = "white"
+ self.build_ui()
+ self.bind_events()
```
-
-Implements a class-based structure for better organization and state management of the painting application.
-
-### Event-Driven Drawing
-```python title="paint.py" showLineNumbers{1}
-self.root.bind("", self.paint)
-self.root.bind("", self.reset)
-
-def paint(self, event):
- x1, y1 = (event.x-2), (event.y-2)
- x2, y2 = (event.x+2), (event.y+2)
- self.canvas.create_oval(x1, y1, x2, y2, fill=self.pen_color,
- outline=self.pen_color, width=self.canvas_width.get())
+A class lets us **stash state** (pen color, brush size, canvas reference) on `self` without sprinkling globals throughout the code.
+
+### 2. Build the layout
+```python title="paint.py"
+def build_ui(self):
+ self.color_frame = LabelFrame(self.root, text="Colors", padx=5, pady=5)
+ self.color_frame.pack(side=LEFT, fill=Y)
+
+ colors = ["#000000", "#ff0000", "#ff8000", "#ffff00",
+ "#00ff00", "#0080ff", "#0000ff", "#8000ff"]
+ for i, color in enumerate(colors):
+ Button(self.color_frame, bg=color, width=3, bd=2,
+ command=lambda c=color: self.set_pen_color(c)).grid(row=i//2, column=i%2)
+
+ self.canvas_width = Scale(self.root, from_=1, to=100, orient=VERTICAL, label="Size")
+ self.canvas_width.set(5)
+ self.canvas_width.pack(side=RIGHT, fill=Y)
+
+ self.canvas = Canvas(self.root, bg="white")
+ self.canvas.pack(fill=BOTH, expand=True)
```
+- `Frame` / `LabelFrame` groups related widgets.
+- The **`command=lambda c=color: ...`** trick is essential: without the default-argument capture, all buttons would end up using the *last* color in the loop.
+- `Scale` is a slider; we use it for brush size.
+- `Canvas` is the drawing surface.
+
+### 3. Bind mouse events
+```python title="paint.py"
+def bind_events(self):
+ self.canvas.bind("", self.paint)
+ self.canvas.bind("", self.reset)
+```
+- `` fires repeatedly while the left mouse button is held and the cursor moves.
+- `` fires once when the button is released.
-Captures mouse movement and button events to create continuous drawing by placing small overlapping ovals.
-
-### Dynamic Color Palette
-```python title="paint.py" showLineNumbers{1}
-colors = ["#ff0000", "#ff4000", "#ff8000", "#ffbf00", "#ffff00", ...]
-i=j=0
-for color in colors:
- Button(self.color_frame, bg=color, bd=2, relief=RIDGE, width=3,
- command=lambda col=color:self.select_color(col)).grid(row=i, column=j)
+### 4. Paint with overlapping ovals
+```python title="paint.py"
+def paint(self, event):
+ size = self.canvas_width.get()
+ x1, y1 = event.x - size, event.y - size
+ x2, y2 = event.x + size, event.y + size
+ self.canvas.create_oval(x1, y1, x2, y2,
+ fill=self.pen_color,
+ outline=self.pen_color)
```
+Every mouse-move event draws a small filled oval at the cursor. Because the events fire many times per second, the result *looks* like a continuous brush stroke. With a bigger brush size, the ovals are larger and the stroke gets wider.
-Creates a comprehensive color palette with buttons arranged in a grid layout for easy color selection.
+> 💡 For a *truly* continuous line, use `create_line` between the previous and current mouse positions. Track the last point in `self.old_x, self.old_y` and reset them in `reset()`.
-### Brush Size Control
-```python title="paint.py" showLineNumbers{1}
-self.canvas_width = Scale(self.root, from_=1, to=100, orient=VERTICAL)
-self.canvas_width.set(1)
-```
+### 5. Color and tool switching
+```python title="paint.py"
+def set_pen_color(self, color):
+ self.pen_color = color
-Provides a vertical scale widget for real-time brush size adjustment from 1 to 100 pixels.
+def use_eraser(self):
+ self.pen_color = self.eraser_color # erase = paint with bg color
-### Canvas Background Customization
-```python title="paint.py" showLineNumbers{1}
-def canvas_color(self):
- color = colorchooser.askcolor(title="Select Color")
- self.canvas.configure(background=color[1])
- self.eraser_color = color[1]
+def pick_canvas_color(self):
+ color = colorchooser.askcolor(title="Canvas color")[1]
+ if color:
+ self.canvas.configure(background=color)
+ self.eraser_color = color
```
+An eraser is just paint that matches the background. Picking a new canvas color updates the eraser to match.
-Allows users to change the canvas background color and automatically updates the eraser color to match.
+### 6. Clear and save
+```python title="paint.py"
+def clear_canvas(self):
+ self.canvas.delete("all")
-### Image Saving with Screen Capture
-```python title="paint.py" showLineNumbers{1}
-def save_paint(self):
+def save_canvas(self):
filename = filedialog.asksaveasfilename(defaultextension=".jpg")
+ if not filename: return
+ self.root.update() # ensure window is fully drawn
x = self.root.winfo_rootx() + self.canvas.winfo_x()
y = self.root.winfo_rooty() + self.canvas.winfo_y()
x1 = x + self.canvas.winfo_width()
y1 = y + self.canvas.winfo_height()
- PIL.ImageGrab.grab().crop((x,y,x1,y1)).save(filename)
+ ImageGrab.grab(bbox=(x, y, x1, y1)).save(filename)
```
+- `delete("all")` removes every drawn item.
+- Saving uses `PIL.ImageGrab.grab` to **screen-capture** the canvas region.
+ - `winfo_rootx/y` give the canvas position in *screen* coordinates (not window coordinates).
+- A more advanced approach maintains a parallel `PIL.Image` and draws into it via `PIL.ImageDraw` — that way the saved image is identical regardless of what is on screen (no risk of capturing an overlapping window).
-Implements screen capture technology to save the canvas content as an image file.
+## Common Mistakes
-## Features
+| Problem | Cause | Fix |
+|---|---|---|
+| All color buttons paint the same color | Closure captured the loop variable | Use `lambda c=color: ...` to bind early |
+| Brush leaves dotted line | Events fire faster than they can be drawn | Use `create_line` between consecutive points |
+| `ImageGrab.grab()` returns the wrong region | Used canvas-local coordinates | Use `winfo_rootx/y` for screen coordinates |
+| Eraser leaves black on a colored canvas | Eraser color hard-coded to white | Update `self.eraser_color` whenever the canvas color changes |
+| Save dialog hangs | No file extension | Use `asksaveasfilename(defaultextension=".jpg")` |
-- **Comprehensive Color Palette**: 27 predefined colors in an organized grid
-- **Variable Brush Size**: Adjustable brush width from 1 to 100 pixels
-- **Eraser Tool**: Switch between drawing and erasing modes
-- **Canvas Customization**: Change background color with color picker
-- **Clear Function**: Reset the entire canvas instantly
-- **Image Saving**: Save artwork as JPEG files
-- **Responsive Drawing**: Smooth, continuous brush strokes
-- **Professional Layout**: Organized toolbar and spacious canvas area
+## Variations to Try
-## Next Steps
+### 1. Custom color picker
+Replace the eight preset buttons with a single "Choose color..." button using `colorchooser.askcolor`.
-### Enhancements
-- Add different brush shapes (square, triangle, spray)
-- Implement undo/redo functionality
-- Create layers support for complex artwork
-- Add text tool for adding labels
-- Implement zoom in/out functionality
-- Add image loading capabilities
-- Create brush opacity controls
-- Add selection tools (rectangle, lasso)
-
-### Learning Extensions
-- Study advanced image processing with PIL/Pillow
-- Explore vector graphics programming
-- Learn about graphics algorithms and optimization
-- Practice with custom widget development
-- Understand color theory and digital art concepts
-- Explore tablet/stylus input integration
+### 2. Smooth brush lines
+```python title="smooth.py"
+def paint(self, event):
+ if self.old_x and self.old_y:
+ self.canvas.create_line(self.old_x, self.old_y,
+ event.x, event.y,
+ width=self.canvas_width.get(),
+ fill=self.pen_color,
+ capstyle=ROUND, smooth=True)
+ self.old_x, self.old_y = event.x, event.y
+
+def reset(self, event):
+ self.old_x = self.old_y = None
+```
+A real, smooth brush stroke.
-## Educational Value
+### 3. Undo / redo
+Maintain a stack of canvas-item IDs. On undo, `canvas.delete(last_id)`.
+
+### 4. Shape tools
+A toolbar with rectangle, oval, and line tools. On mouse-press record the start point; on release create the shape.
+
+### 5. Text tool
+Click on the canvas → open an `Entry` → press Enter → place the text there.
+
+### 6. Spray paint
+Each mouse event creates a random pattern of small dots inside a radius. Looks more like spray than a brush.
+
+### 7. Layers
+Use multiple `Canvas` widgets stacked together, or maintain an in-memory `PIL.Image` per layer and composite on save.
+### 8. Image import
+Use `PIL.Image.open` → `ImageTk.PhotoImage` → `canvas.create_image` to bring an existing picture onto the canvas, then paint over it.
+
+### 9. Touch support
+On a tablet or touch-screen Tkinter receives events the same way. Try it.
+
+### 10. Save as PNG with transparency
+Drop the screen-grab approach and use a parallel `PIL.Image` so the saved PNG truly matches what is drawn — no background bleed.
+
+## Real-World Applications
+- **Image annotation tools** for ML labeling.
+- **Whiteboard apps** for remote teaching.
+- **Quick mockup tools** for designers.
+- **Signature capture** in form-filling apps.
+- **Drawing input** for ML models that take strokes (handwriting recognition, sketch classification).
+
+## Educational Value
This project teaches:
-- **Advanced GUI Development**: Creating complex interfaces with multiple widget types
-- **Event Handling**: Managing mouse events for interactive drawing
-- **Image Processing**: Working with PIL for screen capture and image manipulation
-- **Coordinate Systems**: Understanding pixel-based positioning and drawing
-- **State Management**: Maintaining application state across user interactions
-- **File Operations**: Implementing save/load functionality with file dialogs
-- **Color Theory**: Working with color systems and user color selection
-- **Object-Oriented Design**: Structuring applications using classes and methods
-
-Perfect for understanding graphics programming, user interface design, and image processing concepts in Python.
+- **Event-driven GUI** — bindings, callbacks, the main loop.
+- **Canvas programming** — creating, deleting, and managing graphic items.
+- **Layout managers** — `pack`, `grid`, frames, side-by-side regions.
+- **State management** — what color, what tool, what size, all stored on `self`.
+- **Cross-library bridges** — Tkinter + Pillow to produce a real file.
+- **Class-based design** — keeping UI code organized.
+
+## Next Steps
+- Add **smooth-brush** drawing using `create_line` as shown above.
+- Implement **undo/redo** with a stack of item IDs.
+- Add a **toolbar** with rectangle, oval, line, and text tools.
+- Replace the screen-grab save with a **PIL-backed image** for true layered drawing.
+- Port the UI to **CustomTkinter** for a modern look.
+- Convert it into a **collaborative whiteboard** with WebSockets — pair the GUI with a Flask backend.
+
+## Conclusion
+You built a real desktop paint app: a canvas, a palette, brush controls, save-as-image, and a class-based architecture you can keep extending. Tkinter shows its age in styling but its power model — widgets, events, the main loop — is identical to what big frameworks use. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/paint.py). Find more GUI projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/passwordstrengthchecker.mdx b/src/content/docs/projects/Beginners/passwordstrengthchecker.mdx
index fce505e0..391453d3 100644
--- a/src/content/docs/projects/Beginners/passwordstrengthchecker.mdx
+++ b/src/content/docs/projects/Beginners/passwordstrengthchecker.mdx
@@ -1,6 +1,6 @@
---
title: Password Strength Checker
-description: A Python application that evaluates password strength by checking for various security criteria including length, character types, and complexity requirements.
+description: Build a real password strength checker in Python. Learn regex, entropy calculation, common-password detection, breach-corpus checking, zxcvbn-style scoring, and Have I Been Pwned integration.
sidebar:
order: 39
hero:
@@ -13,220 +13,264 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Password Strength Checker is a Python application that evaluates the strength of passwords based on various security criteria. The application checks for minimum length requirements, presence of uppercase and lowercase letters, numbers, and special characters. It provides immediate feedback to users about password weaknesses and suggestions for improvement. This project demonstrates the use of regular expressions, string analysis, and security best practices in Python programming.
+A password strength checker is more nuanced than it looks. Most tutorials count character classes and call it done — *"has uppercase, has digit, has symbol — great password!"* — which classifies `Password1!` as strong even though it appears in every leaked-credentials dump. In this project you will build the rule-based checker first, then layer on **entropy estimation**, a **common-password blacklist**, a **leak-corpus check** against Have I Been Pwned, and a final score modeled on `zxcvbn`.
+
+You will leave understanding:
+- Why character-class rules are necessary but not sufficient.
+- How to compute password entropy.
+- How `zxcvbn` actually scores passwords.
+- How to query Have I Been Pwned safely using **k-anonymity**.
+- How to give *actionable* feedback, not just a "weak/strong" verdict.
## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
+- Python 3.6 or above.
+- A code editor or IDE.
+- Comfort with regex (`re` module).
+- (Optional) Internet access for the HIBP check.
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download).
+## Getting Started
-Note: This project uses only built-in Python modules (`re` for regular expressions), so no additional installations are required.
+### Create the project
+1. Create folder `password-strength-checker`.
+2. Inside, create `passwordstrengthchecker.py`.
-## Getting Started
-#### Create a Project
-1. Create a folder named `password-strength-checker`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `passwordstrengthchecker.py`.
-4. Copy the given code and paste it in your `passwordstrengthchecker.py` file.
-
-## Write the Code
-1. Copy and paste the following code in your `passwordstrengthchecker.py` file.
+### Write the code
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `password-strength-checker`.
-```cmd title="command" showLineNumbers{1} {1-20}
+
+### Run it
+```cmd title="command"
C:\Users\Your Name\password-strength-checker> python passwordstrengthchecker.py
-Password must contain at least one special character.
-Password must be at least 8 characters long.
-Password must contain at least one uppercase letter.
-Password must contain at least one lowercase letter.
-Password must contain at least one number.
-Password must contain at least one special character.
-Password is strong.
-Password is strong.
+Password: Password123!
+Strong (rule-based)
```
-## Explanation
-1. Import the required module.
-```python title="passwordstrengthchecker.py" showLineNumbers{1} {1}
+## Step-by-Step Explanation
+
+### 1. The rule-based checker
+```python title="passwordstrengthchecker.py"
import re
-```
-2. Define the password strength checking function.
-```python title="passwordstrengthchecker.py" showLineNumbers{1} {1}
-def password_strength_checker(password):
+def rule_based(password: str) -> list[str]:
+ feedback = []
+ if len(password) < 8:
+ feedback.append("Use at least 8 characters.")
+ if not re.search(r"[a-z]", password):
+ feedback.append("Add a lowercase letter.")
+ if not re.search(r"[A-Z]", password):
+ feedback.append("Add an uppercase letter.")
+ if not re.search(r"[0-9]", password):
+ feedback.append("Add a digit.")
+ if not re.search(r"[^A-Za-z0-9]", password):
+ feedback.append("Add a symbol.")
+ return feedback
```
+A list of issues is more useful than a single boolean — the user can fix multiple problems in one revision.
-3. Check minimum length requirement.
-```python title="passwordstrengthchecker.py" showLineNumbers{1} {1-2}
-if len(password) < 8:
- print("Password must be at least 8 characters long.")
+### 2. Strong vs. weak verdict
+```python title="passwordstrengthchecker.py"
+def strength(password: str) -> tuple[str, list[str]]:
+ issues = rule_based(password)
+ return ("Strong" if not issues else "Weak"), issues
```
-4. Check for lowercase letters using regular expressions.
-```python title="passwordstrengthchecker.py" showLineNumbers{1} {1-2}
-elif not re.search("[a-z]", password):
- print("Password must contain at least one lowercase letter.")
+### 3. Run it
+```python title="passwordstrengthchecker.py"
+for p in ["Password", "password123", "PASSWORD123",
+ "Password@", "Password123@"]:
+ verdict, issues = strength(p)
+ print(f"{p}: {verdict}")
+ for i in issues: print(f" - {i}")
```
-5. Check for uppercase letters.
-```python title="passwordstrengthchecker.py" showLineNumbers{1} {1-2}
-elif not re.search("[A-Z]", password):
- print("Password must contain at least one uppercase letter.")
+## Why Rules Are Not Enough
+
+`Password1!` satisfies every rule above. So does `Qwerty12$`. They are also among the **most common leaked passwords** on Earth. Real strength assessment needs two more inputs:
+
+1. **Common-password lists** — reject anything in the top-N (try `rockyou.txt`, ~14 million).
+2. **Entropy** — measure how much information the password actually carries.
+
+## Entropy Estimation
+
+```python title="entropy.py"
+import math, string
+def entropy_bits(password: str) -> float:
+ pool = 0
+ if any(c.islower() for c in password): pool += 26
+ if any(c.isupper() for c in password): pool += 26
+ if any(c.isdigit() for c in password): pool += 10
+ if any(c in string.punctuation for c in password): pool += len(string.punctuation)
+ return len(password) * math.log2(max(pool, 1))
```
+Rough rubric:
+- **< 28 bits** — very weak (crackable in seconds).
+- **28 – 35** — weak (offline crack in minutes).
+- **36 – 59** — reasonable for low-value accounts.
+- **60 – 127** — strong.
+- **≥ 128** — paranoid / master-vault-grade.
+
+A 16-character mix of all four classes lands around 105 bits — well into "strong" territory.
+
+## Common-Password Blacklist
+
+The simplest meaningful upgrade. Get a list (e.g. SecLists' `10-million-password-list-top-1000000.txt`) and:
+```python title="blacklist.py"
+from pathlib import Path
+COMMON = set(Path("top-passwords.txt").read_text().splitlines())
-6. Check for numbers.
-```python title="passwordstrengthchecker.py" showLineNumbers{1} {1-2}
-elif not re.search("[0-9]", password):
- print("Password must contain at least one number.")
+def is_common(password: str) -> bool:
+ return password.lower() in COMMON
```
+Set lookup is O(1). One million entries fits in ~30 MB of RAM — fine for a desktop tool.
+
+## Have I Been Pwned (k-Anonymity)
+
+[HIBP](https://haveibeenpwned.com/API/v3#PwnedPasswords) lets you check a password against billions of leaked entries **without sending the password itself**:
+
+1. SHA-1 your password.
+2. Send only the **first 5 hex characters** of the hash.
+3. Server returns all hashes starting with that prefix (about 500 per query).
+4. You check locally whether the full hash is in that list.
+
+```python title="hibp.py"
+import hashlib, requests
-7. Check for special characters.
-```python title="passwordstrengthchecker.py" showLineNumbers{1} {1-2}
-elif not re.search("[_@$]", password):
- print("Password must contain at least one special character.")
+def hibp_count(password: str) -> int:
+ sha1 = hashlib.sha1(password.encode("utf-8")).hexdigest().upper()
+ prefix, suffix = sha1[:5], sha1[5:]
+ r = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}", timeout=10)
+ r.raise_for_status()
+ for line in r.text.splitlines():
+ s, count = line.split(":")
+ if s == suffix:
+ return int(count)
+ return 0
```
+A non-zero count means the password appears in known breaches — *reject it immediately*. Even if it scores high on rules and entropy, attackers will try it first.
-8. Confirm strong password.
-```python title="passwordstrengthchecker.py" showLineNumbers{1} {1-2}
-else:
- print("Password is strong.")
+## Putting It All Together
+
+```python title="full_check.py"
+def full_check(password: str):
+ issues = rule_based(password)
+ bits = entropy_bits(password)
+ common = is_common(password)
+ try:
+ breaches = hibp_count(password)
+ except Exception:
+ breaches = None
+
+ score = 0
+ if len(password) >= 12: score += 1
+ if not issues: score += 1
+ if bits >= 60: score += 1
+ if not common: score += 1
+ if breaches == 0: score += 1
+
+ levels = ["Very Weak", "Weak", "Fair", "Good", "Strong", "Excellent"]
+ return {
+ "level": levels[score],
+ "bits": round(bits, 1),
+ "issues": issues,
+ "common": common,
+ "breaches": breaches,
+ }
```
+Now `full_check("Password1!")` says "Weak — found in 1.6 million breaches" instead of "Strong — has all four character classes."
+
+## Why You Should Recommend `zxcvbn`
-9. Test the function with various passwords.
-```python title="passwordstrengthchecker.py" showLineNumbers{1} {1-7}
-password_strength_checker("Password123")
-password_strength_checker("Password")
-password_strength_checker("password123")
-password_strength_checker("PASSWORD123")
-password_strength_checker("Password@")
-password_strength_checker("Password123@")
-password_strength_checker("Password123@!")
+For a real product use **`zxcvbn-python`** (a Python port of Dropbox's library):
+```bash title="install"
+pip install zxcvbn
```
+```python title="zxcvbn_use.py"
+from zxcvbn import zxcvbn
+r = zxcvbn("Tr0ub4dor&3")
+print(r["score"], r["feedback"]["warning"], r["crack_times_display"])
+```
+It models real attacker behavior — dictionary attacks, l33t substitutions, keyboard patterns, dates, repeats — and gives a 0-4 score plus an estimated time to crack at four attack scenarios. Your rule-based code is *educational*; `zxcvbn` is *production*.
-## Security Criteria Checked
-The application evaluates passwords based on these criteria:
+## Common Mistakes
-### 1. **Length Requirement**
-- Minimum 8 characters
-- Longer passwords provide better security
+| Problem | Cause | Fix |
+|---|---|---|
+| `Password1!` rated strong | Only checked character classes | Add common-password + breach check |
+| HIBP request fails | No internet | Make the breach check optional, surface as "unknown" |
+| Sending password over the network | Naive API design | Use the k-anonymity range API |
+| Locale-specific punctuation missed | Used a fixed regex | Use `re.search(r"[^A-Za-z0-9]", password)` for "any non-alphanumeric" |
+| Logs/print contain the password | Debugging left in | Never log passwords; never even print echoes |
+| Comparing in plain text | Custom logic | Always hash before comparing/storing |
-### 2. **Lowercase Letters**
-- Must contain at least one lowercase letter (a-z)
-- Increases character set complexity
+## Variations to Try
-### 3. **Uppercase Letters**
-- Must contain at least one uppercase letter (A-Z)
-- Adds to password entropy
+### 1. Pattern detection
+Reject keyboard runs (`qwerty`, `asdf`), repeated characters (`aaa`), and sequences (`12345`).
+```python title="pattern.py"
+import re
+def has_runs(p):
+ return bool(re.search(r"(.)\1\1", p)) or bool(re.search(r"012|123|234|abc|qwe", p.lower()))
+```
-### 4. **Numbers**
-- Must contain at least one digit (0-9)
-- Numerical characters increase complexity
+### 2. Dates and years
+A 4-digit substring matching `19xx` or `20xx` is almost certainly a birth year — flag it.
-### 5. **Special Characters**
-- Must contain at least one special character (_, @, $)
-- Note: Current implementation checks for limited special characters
+### 3. Personal-info check
+Accept the user's name, email, and birth date; reject any password containing those.
-## Features
-- **Comprehensive Checking:** Evaluates multiple security criteria
-- **Immediate Feedback:** Provides specific weakness identification
-- **Regular Expression Power:** Uses regex for pattern matching
-- **Simple Interface:** Easy-to-use function-based approach
-- **Batch Testing:** Can test multiple passwords at once
-- **Clear Messages:** Specific feedback for each security requirement
+### 4. GUI version
+A Tkinter form with a colored progress bar that shifts red → yellow → green as the user types. See [Calculator GUI](./calculatorgui) for the layout pattern.
-## Regular Expression Patterns
-The application uses these regex patterns:
+### 5. Web frontend
+Flask + a textarea (see [Basic Web Server](./basicwebserver)). **Never** ship the password to the server unless you absolutely must; do the check in the browser with JS.
-- **`[a-z]`:** Matches any lowercase letter
-- **`[A-Z]`:** Matches any uppercase letter
-- **`[0-9]`:** Matches any digit
-- **`[_@$]`:** Matches specific special characters
+### 6. Estimated crack time
+Multiply entropy by hashes-per-second assumptions:
+- Online (1 000 guesses/s): how long to brute force?
+- Offline GPU (1 trillion guesses/s): how long?
-## Sample Test Results
-```
-Password123 → Missing special character
-Password → Too short, missing numbers and special characters
-password123 → Missing uppercase and special characters
-PASSWORD123 → Missing lowercase and special characters
-Password@ → Missing numbers
-Password123@ → Strong password ✓
-Password123@! → Strong password ✓
-```
+### 7. Suggest a fix
+If the password is too short, append a random word. If a class is missing, insert one. If common, regenerate entirely.
-## Next Steps
-You can enhance this project by:
-- Adding a GUI interface using Tkinter
-- Implementing a scoring system (weak, medium, strong)
-- Expanding special character requirements
-- Adding checks for common passwords
-- Implementing password generation suggestions
-- Adding dictionary word detection
-- Creating visual strength indicators
-- Adding entropy calculation
-- Implementing sequential character detection
-- Adding customizable security requirements
-
-## Enhanced Version Ideas
-```python title="passwordstrengthchecker.py" showLineNumbers{1}
-def enhanced_password_checker(password):
- score = 0
- feedback = []
-
- # Length scoring
- if len(password) >= 12:
- score += 2
- elif len(password) >= 8:
- score += 1
- else:
- feedback.append("Password too short")
-
- # Character type scoring
- if re.search("[a-z]", password):
- score += 1
- if re.search("[A-Z]", password):
- score += 1
- if re.search("[0-9]", password):
- score += 1
- if re.search("[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>\/?]", password):
- score += 2
-
- # Return strength level
- if score >= 7:
- return "Very Strong"
- elif score >= 5:
- return "Strong"
- elif score >= 3:
- return "Medium"
- else:
- return "Weak"
+### 8. Pair with the generator
+Cross-reference with [Random Password Generator](./randompasswordgenerator). Show the strength of every generated password.
+
+### 9. Account-wide policy
+Read a config file specifying minimums (length, classes, entropy, max breach count) and enforce them.
+
+### 10. CLI tool
+```bash title="cli"
+strength check "Tr0ub4dor&3"
+strength suggest --length 16 --no-ambig
```
-## Common Password Weaknesses
-- **Too short:** Less than 8 characters
-- **Single case:** Only uppercase or lowercase
-- **No numbers:** Missing numerical characters
-- **No symbols:** Missing special characters
-- **Dictionary words:** Common words that can be found in dictionaries
-- **Personal info:** Names, birthdays, addresses
-- **Sequential patterns:** 123456, abcdef, qwerty
-
-## Security Best Practices
-- **Use unique passwords:** Different password for each account
-- **Enable 2FA:** Two-factor authentication when available
-- **Regular updates:** Change passwords periodically
-- **Use password managers:** Store complex passwords securely
-- **Avoid personal info:** Don't use easily guessable information
+## Security Best Practices the Tool Should Reinforce
+- **Length over complexity.** A 16-character random passphrase beats `Aa1!Aa1!`.
+- **Unique per service.** Even a strong password is weak if reused across breached sites.
+- **Never trust client-side checks alone.** A web app must enforce policy on the server too.
+- **Treat the input as a secret.** No logging, no caching, no analytics tracking the actual value.
+- **Hash + salt server-side.** Never store passwords plaintext.
+
+## Real-World Applications
+- Signup forms with real-time strength feedback.
+- Password-change flows that reject reuse.
+- IT admin tools that audit a company's password hashes.
+- Onboarding wizards that teach users *why* `Password1!` is weak.
+- Self-service password reset with leak-corpus protection.
## Educational Value
-This project teaches:
-- **Regular expressions:** Pattern matching in strings
-- **Conditional logic:** Multiple if-elif-else statements
-- **String analysis:** Character type identification
-- **Security concepts:** Password strength principles
-- **Function design:** Modular code organization
+- **Regex** — pattern matching across character classes.
+- **Entropy math** — turning intuition into a number.
+- **k-anonymity** — privacy-preserving lookup design (a wonderful idea worth knowing).
+- **API integration** — calling a real third-party service safely.
+- **UX of security** — actionable feedback vs. red-X verdicts.
+
+## Next Steps
+- Implement the **common-password blacklist** above.
+- Add **HIBP k-anonymity** check.
+- Switch to **`zxcvbn`** for production scoring.
+- Add **pattern detection** (sequences, runs, dates).
+- Build a **GUI** with a colored progress bar.
+- Pair with [Random Password Generator](./randompasswordgenerator) for a complete password tooling pair.
## Conclusion
-In this project, we learned how to create a Password Strength Checker using Python's regular expression module. We explored pattern matching techniques, security criteria evaluation, and user feedback systems. This project provides valuable insights into cybersecurity practices and demonstrates practical applications of string analysis in Python. The concepts learned here can be extended to more advanced security applications and validation systems. To find more projects like this, you can visit Python Central Hub.
+You built a rule-based password checker, learned why the rules alone are dangerously incomplete, and added entropy + blacklist + breach-corpus checks that turn the tool into something you would actually trust. Real password security is a deceptively deep field, and even a small checker can do a real job — or quietly mislead users. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/passwordstrengthchecker.py). Find more security-focused projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/personaldiary.mdx b/src/content/docs/projects/Beginners/personaldiary.mdx
index f0ac1697..b4a27dab 100644
--- a/src/content/docs/projects/Beginners/personaldiary.mdx
+++ b/src/content/docs/projects/Beginners/personaldiary.mdx
@@ -1,6 +1,6 @@
---
title: Personal Diary Application
-description: Create a digital diary with entry management, search functionality, and mood tracking
+description: Build a CLI diary in Python with mood tracking, full-text search, statistics, and JSON persistence. Learn OOP, file I/O, dataclasses, datetime, and how to grow it into an encrypted journal.
sidebar:
order: 55
hero:
@@ -14,150 +14,279 @@ import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Build a comprehensive personal diary application that allows users to create, store, search, and analyze their daily thoughts and experiences. Features include mood tracking, search functionality, statistics, and persistent JSON storage.
+A personal diary is one of those projects where everything you build is genuinely useful. In this tutorial you will build a command-line journal that lets you write timestamped entries, tag each one with a mood, search through your past, filter by mood, and see stats on your journaling habit. Entries persist between runs via a JSON file you control entirely.
-## Prerequisites
+You will learn:
+- How to model entries as a class (and how `@dataclass` cuts that boilerplate).
+- How to read and write JSON with `json.dump` / `json.load`.
+- How to work with timestamps via `datetime`.
+- How to build a clean, menu-driven CLI without spaghetti.
+- How to add real privacy with optional encryption.
-- Basic understanding of Python syntax
-- Knowledge of file operations and JSON handling
-- Familiarity with datetime operations
-- Understanding of object-oriented programming
-- Basic knowledge of data structures (lists, dictionaries)
+## Prerequisites
+- Python 3.6 or above (3.7+ recommended for `dataclasses`).
+- A text editor or IDE.
+- Comfort with functions, lists, and dictionaries.
+- Familiarity with running a `.py` file from the terminal.
+
+## Concepts You Will Use
+
+| Concept | Purpose |
+|---|---|
+| **Class** | Group related data (entry fields) and behavior. |
+| **`@dataclass`** | Auto-generate `__init__`, `__repr__`, comparison — less boilerplate. |
+| **`json.dump` / `json.load`** | Persist Python objects to a text file as JSON. |
+| **`datetime`** | Timestamp entries with date and time. |
+| **`Path` / `os.path.exists`** | Detect whether the diary file is new or existing. |
+| **List comprehensions** | Filter and search entries cleanly. |
## Getting Started
+### Create the project
+1. Create a folder named `personal-diary`.
+2. Inside it, create `personaldiary.py`.
+3. (Optional) create `diary.json` — the script will create it for you on first save.
+### Write the code
+
-1. **No External Dependencies Required**
- ```bash
- # Uses only built-in Python modules
- ```
+### Run it
+```bash title="run"
+python personaldiary.py
+```
-2. **Run the Personal Diary**
- ```bash
- python personaldiary.py
- ```
+```
+1. Add new entry
+2. View all entries
+3. Search entries
+4. Filter by mood
+5. View statistics
+6. Exit
+Choice: 1
+Title: First entry
+Mood (happy/sad/neutral/excited/anxious): happy
+Content (end with empty line):
+> Started my Python diary today.
+>
+Saved.
+```
-3. **Start Journaling**
- - Add new entries with title and content
- - Select mood for each entry
- - Use search to find specific entries
- - View statistics about your journaling habits
+## Step-by-Step Explanation
+### 1. The data model
+```python title="personaldiary.py"
+from dataclasses import dataclass, field, asdict
+from datetime import datetime
+@dataclass
+class DiaryEntry:
+ title: str
+ content: str
+ mood: str = "neutral"
+ date: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
-## Code Explanation
+ def to_dict(self):
+ return asdict(self)
-### Entry Data Structure
-```python title="personaldiary.py" showLineNumbers{1}
-class DiaryEntry:
- def __init__(self, date, title, content, mood="neutral"):
- self.date = date
- self.title = title
- self.content = content
- self.mood = mood
+ @classmethod
+ def from_dict(cls, d):
+ return cls(**d)
```
+Why `@dataclass`? In one decorator you get:
+- A real `__init__` that accepts each field.
+- A `__repr__` for clean printing.
+- Equality based on field values.
+- Optional default values.
+
+`field(default_factory=...)` is the right way to default a mutable or computed value — it runs each time a new entry is created, so every entry gets its own current timestamp.
+
+### 2. The Diary class
+```python title="personaldiary.py"
+import json
+from pathlib import Path
+
+class Diary:
+ def __init__(self, filename="diary.json"):
+ self.filename = Path(filename)
+ self.entries: list[DiaryEntry] = []
+ self.load()
+
+ def load(self):
+ if self.filename.exists():
+ data = json.loads(self.filename.read_text(encoding="utf-8"))
+ self.entries = [DiaryEntry.from_dict(d) for d in data]
+
+ def save(self):
+ data = [e.to_dict() for e in self.entries]
+ self.filename.write_text(json.dumps(data, indent=2), encoding="utf-8")
+
+ def add(self, entry: DiaryEntry):
+ self.entries.append(entry)
+ self.save()
+```
+Reading and writing the *whole* file each time is fine for personal use — JSON is small and humans like being able to open it in a text editor. If you ever had millions of entries you would swap to SQLite.
-Encapsulates diary entries with timestamp, title, content, and mood tracking for comprehensive journaling.
-
-### JSON Persistence
-```python title="personaldiary.py" showLineNumbers{1}
-def save_entries(self):
- with open(self.filename, 'w') as f:
- json.dump([entry.to_dict() for entry in self.entries], f, indent=2)
+### 3. Search and filter
+```python title="personaldiary.py"
+def search(self, keyword: str) -> list[DiaryEntry]:
+ k = keyword.lower()
+ return [e for e in self.entries
+ if k in e.title.lower() or k in e.content.lower()]
-def load_entries(self):
- if os.path.exists(self.filename):
- with open(self.filename, 'r') as f:
- data = json.load(f)
- self.entries = [DiaryEntry.from_dict(entry) for entry in data]
+def by_mood(self, mood: str) -> list[DiaryEntry]:
+ return [e for e in self.entries if e.mood == mood]
```
-
-Implements data persistence using JSON format for cross-platform compatibility and human readability.
-
-### Search Functionality
-```python title="personaldiary.py" showLineNumbers{1}
-def search_entries(self, keyword):
- found_entries = []
- keyword_lower = keyword.lower()
-
- for entry in self.entries:
- if (keyword_lower in entry.title.lower() or
- keyword_lower in entry.content.lower()):
- found_entries.append(entry)
+List comprehensions read like English. The `lower()` calls make search case-insensitive.
+
+### 4. Statistics
+```python title="personaldiary.py"
+from collections import Counter
+
+def stats(self):
+ if not self.entries:
+ return {"total": 0, "moods": {}}
+ moods = Counter(e.mood for e in self.entries)
+ return {
+ "total": len(self.entries),
+ "moods": {m: f"{c} ({c/len(self.entries):.1%})"
+ for m, c in moods.most_common()},
+ "first": self.entries[0].date,
+ "latest": self.entries[-1].date,
+ }
```
-
-Provides case-insensitive search across both titles and content for easy entry retrieval.
-
-### Mood Analytics
-```python title="personaldiary.py" showLineNumbers{1}
-def get_statistics(self):
- mood_counts = {}
- for entry in self.entries:
- mood = entry.mood
- mood_counts[mood] = mood_counts.get(mood, 0) + 1
-
- for mood, count in mood_counts.items():
- percentage = (count / total_entries) * 100
- print(f" {mood}: {count} ({percentage:.1f}%)")
+`Counter` is the right tool any time you find yourself reaching for `dict` plus `+=`.
+
+### 5. The menu loop
+```python title="personaldiary.py"
+def main():
+ diary = Diary()
+ while True:
+ print("\n1. Add 2. View 3. Search 4. By mood 5. Stats 6. Exit")
+ choice = input("Choice: ").strip()
+ if choice == "1":
+ add_entry(diary)
+ elif choice == "2":
+ view_all(diary)
+ elif choice == "3":
+ search_entries(diary)
+ elif choice == "4":
+ filter_by_mood(diary)
+ elif choice == "5":
+ show_stats(diary)
+ elif choice == "6":
+ break
+ else:
+ print("Invalid choice.")
```
+Each menu option calls a separate helper function. Each helper does *one* thing. The main loop only routes.
+
+### 6. Multi-line content
+```python title="personaldiary.py"
+def read_multiline(prompt: str) -> str:
+ print(prompt + " (end with an empty line)")
+ lines = []
+ while True:
+ line = input("> ")
+ if line == "":
+ break
+ lines.append(line)
+ return "\n".join(lines)
+```
+This pattern lets users write paragraphs, not just one line. Press Enter on an empty line to finish.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `json.decoder.JSONDecodeError` on first run | File exists but is empty | Check `if self.filename.exists() and self.filename.stat().st_size > 0` |
+| Entries lost after restart | Forgot `self.save()` after adding | Save on every mutating operation, not just exit |
+| Unicode errors | Default encoding mismatch | Always pass `encoding="utf-8"` to read/write |
+| `KeyError` after upgrading the schema | Old file lacks a new field | Use `.get()` in `from_dict` or run a one-shot migration |
+| Search returns nothing | Case mismatch | `.lower()` both sides |
+
+## Variations to Try
+
+### 1. Edit and delete entries
+Number entries when displaying them; let the user type the number to edit or delete.
+
+### 2. Encrypted entries
+A diary should be private. Use **`cryptography.fernet`**:
+```python title="encrypt.py"
+from cryptography.fernet import Fernet
+key = Fernet(...).generate_key() # store separately!
+cipher = Fernet(key)
+encrypted = cipher.encrypt(content.encode())
+```
+Save `encrypted` instead of the raw content. Decrypt on load. Better: derive the key from a user-provided password using `PBKDF2HMAC`.
+
+### 3. Date filtering
+"Show entries from last week" or "from 2025-01-01 to 2025-01-31".
+```python title="date_range.py"
+from datetime import datetime
+start = datetime.fromisoformat("2025-01-01")
+end = datetime.fromisoformat("2025-01-31")
+return [e for e in self.entries
+ if start <= datetime.fromisoformat(e.date) <= end]
+```
+
+### 4. Tags / categories
+Add a `tags: list[str]` field; let the user filter by tag the same way as by mood.
+
+### 5. Sentiment analysis
+Use **TextBlob** or **VADER** to compute a sentiment score per entry; correlate it with your self-reported mood.
-Analyzes mood patterns and provides statistical insights into emotional trends over time.
-
-### Interactive Menu System
-```python title="personaldiary.py" showLineNumbers{1}
-while True:
- print("1. Add new entry")
- print("2. View all entries")
- print("3. Search entries")
- print("4. Filter by mood")
- print("5. View statistics")
- print("6. Exit")
+### 6. Markdown rendering
+Save entries as Markdown. Export the diary as a single Markdown or HTML file you can print.
+
+### 7. GUI version
+Build a Tkinter UI with a list of dates on the left and the content on the right.
+
+### 8. Cloud backup
+Sync the `diary.json` file to S3 / Google Drive on save. Encrypt before upload.
+
+### 9. Mood chart
+Use **matplotlib** to plot moods over time:
+```python title="chart.py"
+import matplotlib.pyplot as plt
+dates = [e.date for e in diary.entries]
+moods = [e.mood for e in diary.entries]
+plt.plot(dates, moods, marker="o")
+plt.xticks(rotation=45)
+plt.tight_layout(); plt.show()
```
-Implements user-friendly command-line interface with clear navigation options.
+### 10. Reminder
+Schedule a daily prompt at 9 PM to write today's entry. See [Basic Alarm Clock](./basicalarmclock) for the scheduling idea.
-## Features
+## Privacy Considerations
-- **Entry Management**: Create, view, and organize diary entries
-- **Mood Tracking**: Record and analyze emotional states over time
-- **Search Functionality**: Find entries by keywords in title or content
-- **Mood Filtering**: View entries by specific emotional states
-- **Statistics Dashboard**: Analyze journaling patterns and mood trends
-- **Persistent Storage**: Automatic saving and loading via JSON files
-- **Date Tracking**: Automatic timestamp recording for all entries
-- **Multi-line Content**: Support for detailed, lengthy diary entries
+A diary on disk is plaintext by default. If you keep anything personal:
+- **Move the diary file out of any auto-synced folder** (Dropbox, OneDrive, iCloud) unless those services are encrypted at rest with your key.
+- **Encrypt at rest** with `cryptography.fernet` and a password-derived key.
+- **Never commit your diary file to Git**. Add `diary.json` to `.gitignore`.
+- **Back up the file** somewhere safe — losing it is permanent.
-## Next Steps
+## Real-World Applications
-### Enhancements
-- Add entry editing and deletion capabilities
-- Implement password protection and encryption
-- Create backup and export functionality
-- Add photo/image attachments to entries
-- Implement reminder system for regular journaling
-- Create mood visualization with charts
-- Add entry templates for different occasions
-- Implement cloud sync capabilities
-
-### Learning Extensions
-- Study data visualization libraries (matplotlib, plotly)
-- Explore encryption for privacy protection
-- Learn about database integration (SQLite)
-- Practice with GUI development using Tkinter
-- Understand natural language processing for sentiment analysis
-- Explore mobile app development for cross-platform access
+- **Personal journaling** — what this is.
+- **Mood / habit tracking** — bullet-journal-style logs.
+- **Reading log** or **workout log** — same data structure, different fields.
+- **Customer-service ticketing** prototypes — entries are tickets, moods are priorities.
+- **Bug-tracking** for solo projects.
## Educational Value
+- **Data modeling** with `@dataclass`.
+- **Persistence** with JSON, including schema evolution.
+- **Searching and filtering** with list comprehensions.
+- **Counting and aggregation** with `Counter`.
+- **CLI design** — clear menus, helper functions, no global state.
-This project teaches:
-- **Data Persistence**: Saving and loading application data across sessions
-- **Object-Oriented Design**: Creating classes for data modeling and organization
-- **JSON Processing**: Working with structured data formats
-- **Search Algorithms**: Implementing text search and filtering functionality
-- **Statistical Analysis**: Computing and presenting data insights
-- **User Interface Design**: Creating intuitive command-line interactions
-- **File Management**: Handling file operations and error management
-- **Date/Time Handling**: Working with timestamps and date formatting
-
-Perfect for understanding data management, user interaction design, and building personal productivity applications.
+## Next Steps
+- Build the **encryption layer** (`cryptography.fernet`).
+- Add a **edit/delete** flow.
+- Plot mood over time with **matplotlib**.
+- Wrap with a **Tkinter GUI** or a **Flask web app**.
+- Migrate storage from JSON to **SQLite** when you cross a few thousand entries.
+
+## Conclusion
+You built a real productivity tool: timestamped, searchable, statistically aware. The same architecture (data class + persistence + menu loop) underlies thousands of small CLI tools used in the wild. The full source is on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/personaldiary.py). Explore more on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/quizapp.mdx b/src/content/docs/projects/Beginners/quizapp.mdx
index efba13ca..908e163f 100644
--- a/src/content/docs/projects/Beginners/quizapp.mdx
+++ b/src/content/docs/projects/Beginners/quizapp.mdx
@@ -1,6 +1,6 @@
---
title: Basic Quiz Game
-description: An interactive Python quiz application built with Tkinter featuring multiple-choice questions, score tracking, and navigation controls for an engaging learning experience.
+description: Build a Tkinter quiz app in Python with multiple-choice questions, shuffling, scoring, timer, JSON question bank, and a path to multi-category quizzes with explanations and persistence.
sidebar:
order: 22
hero:
@@ -13,262 +13,291 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Basic Quiz Game is an interactive Python application that presents multiple-choice questions about world capitals through a graphical user interface. Built with Tkinter, the application features question navigation, score tracking, answer validation, and various control options including shuffle, reset, and back functionality. This project demonstrates GUI development, data management, user interaction handling, and educational software design principles.
+A quiz app touches everything: UI widgets, state machines, randomization, scoring, navigation, and (with a bit of work) persistent question banks. In this tutorial you will build a Tkinter quiz game on world capitals with 10 multiple-choice questions, then refactor it from hard-coded arrays into a clean `Question` class, load questions from JSON, add a countdown timer, support multiple categories, and finish with a sketch of a results screen that explains the wrong answers.
+
+You will leave understanding:
+- The radio-button + StringVar pattern for multiple-choice input.
+- Per-question state vs. global score state.
+- Why parallel arrays (`questions[]`, `options[]`, `answers[]`) are fragile and how to fix it.
+- How to load a JSON question bank.
+- How to build a non-blocking countdown timer with `after`.
## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
-- tkinter module (usually comes pre-installed)
+- Python 3.6 or above.
+- A text editor or IDE.
+- Tkinter (ships with Python; Linux: `sudo apt install python3-tk`).
+- Comfort with Tkinter basics. [Calculator GUI](./calculatorgui) is a softer first project.
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download).
+## Getting Started
-Note: This project uses only built-in Python modules, so no additional installations are required.
+### Create the project
+1. Create folder `quiz-game`.
+2. Inside, create `quizapp.py`.
-## Getting Started
-#### Create a Project
-1. Create a folder named `quiz-game`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `quizapp.py`.
-4. Copy the given code and paste it in your `quizapp.py` file.
-
-## Write the Code
-1. Copy and paste the following code in your `quizapp.py` file.
-
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `quiz-game`.
-```cmd title="command" showLineNumbers{1} {1-15}
+### Write the code
+
+
+### Run it
+```cmd title="command"
C:\Users\Your Name\quiz-game> python quizapp.py
-# A GUI window will open with:
-# - Question display area
-# - Four multiple-choice radio buttons
-# - Navigation buttons (Next, Back, Reset, Shuffle)
-# - Show Score button
-# - Score tracking functionality
-
-# Example quiz flow:
-Question 1: "What is the Capital of India?"
-Options: New Delhi, Mumbai, Kolkata, Chennai
-Select answer → Click Next → Continue to next question
-Final Score: "Your Score is 8/10"
+# Window opens with:
+# - Question text at the top
+# - 4 radio buttons for choices
+# - Next / Back / Shuffle / Reset / Show Score buttons
```
-## Explanation
-
-### Code Structure Overview
-The quiz application is organized into several key components:
+## Step-by-Step Explanation
-1. **Data Structure:** Questions, options, and answers arrays
-2. **GUI Components:** Labels, radio buttons, and control buttons
-3. **Navigation Logic:** Forward/backward movement through questions
-4. **Score Management:** Tracking correct answers and displaying results
-
-### Data Setup
-1. **Import modules and create main window.**
-```python title="quizapp.py" showLineNumbers{1} {1-8}
+### 1. Imports and root window
+```python title="quizapp.py"
from tkinter import *
from tkinter import messagebox
import random
root = Tk()
root.title("Quiz App")
-root.geometry("500x500")
-root.resizable(0, 0)
+root.geometry("520x420")
+root.resizable(False, False)
```
+A fixed-size window keeps the layout predictable.
-2. **Initialize quiz data structures.**
-```python title="quizapp.py" showLineNumbers{1} {1-10}
+### 2. The question bank
+```python title="quizapp.py"
questions = [
"What is the Capital of India?",
"What is the Capital of USA?",
- "What is the Capital of UK?",
- # ... more questions
+ # … 8 more
]
-
options = [
["New Delhi", "Mumbai", "Kolkata", "Chennai"],
["New York", "Washington DC", "California", "Texas"],
- # ... corresponding options
+ # … 8 more lists
]
-
-answers = [0, 1, 0, 0, 0, 0, 0, 0, 0, 0] # Index of correct answers
+answers = [0, 1, 0, 0, 4, 0, 0, 1, 0, 0] # index of the correct option per question
```
+This **parallel-arrays** layout works but is fragile — changing the order of one list without the others silently corrupts the quiz. The refactor below fixes it.
-3. **Core navigation and scoring logic.**
-```python title="quizapp.py" showLineNumbers{1} {1-15}
-def selected():
- global ans, ques_num, score, ans_list
- if ques_num == 10:
- submit.config(text="Finish")
- if ans == answers[ques_num - 1]:
+### 3. State variables
+```python title="quizapp.py"
+selected = IntVar(value=-1)
+current = 0
+score = 0
+```
+`IntVar` is Tkinter's wrapper that lets the same value drive multiple widgets at once — radio buttons share it.
+
+### 4. UI widgets
+```python title="quizapp.py"
+question_lbl = Label(root, text=questions[current],
+ font=("Arial", 16, "bold"), wraplength=480, pady=10)
+question_lbl.pack(pady=10)
+
+radios = []
+for i in range(4):
+ rb = Radiobutton(root, text=options[current][i], variable=selected,
+ value=i, font=("Arial", 12), anchor="w", padx=20)
+ rb.pack(fill="x")
+ radios.append(rb)
+```
+All four radios share `selected`. Whichever one the user clicks sets `selected.get()` to that radio's `value=`.
+
+### 5. Navigation
+```python title="quizapp.py"
+def show_question(idx):
+ question_lbl.config(text=questions[idx])
+ for i, rb in enumerate(radios):
+ rb.config(text=options[idx][i])
+ selected.set(-1) # clear selection
+
+def next_q():
+ global current, score
+ if selected.get() == -1:
+ return messagebox.showwarning("Pick one", "Select an option first.")
+ if selected.get() == answers[current]:
score += 1
- ans_list.append(1)
- else:
- ans_list.append(0)
- ques_num += 1
- ques.config(text=questions[ques_num - 1])
- for i in range(4):
- radio[i].config(text=options[ques_num - 1][i])
- if ques_num == 11:
- messagebox.showinfo("Score", f"Your Score is {score}")
+ current += 1
+ if current >= len(questions):
+ messagebox.showinfo("Done", f"Your score: {score}/{len(questions)}")
root.destroy()
+ return
+ show_question(current)
+
+def shuffle():
+ global current, score
+ combined = list(zip(questions, options, answers))
+ random.shuffle(combined)
+ for i, (q, o, a) in enumerate(combined):
+ questions[i], options[i], answers[i] = q, o, a
+ current = 0; score = 0
+ show_question(current)
```
+`zip(questions, options, answers)` is the cleanest way to keep the parallel arrays in sync during a shuffle — pair them up, shuffle once, unzip back.
-## Features
-- **10 Geography Questions:** World capitals quiz covering major countries
-- **Multiple Choice Interface:** Four options per question with radio button selection
-- **Score Tracking:** Real-time scoring with final results display
-- **Navigation Controls:** Next, Back, Reset, and Shuffle functionality
-- **Question Shuffling:** Randomize question order for variety
-- **Answer Validation:** Automatic checking and score calculation
-- **User-Friendly Interface:** Clean design with intuitive controls
-
-## Quiz Questions Covered
-The application includes questions about capitals of:
-1. **India** → New Delhi
-2. **USA** → Washington DC
-3. **UK** → London
-4. **China** → Beijing
-5. **Japan** → Tokyo
-6. **Russia** → Moscow
-7. **France** → Paris
-8. **Germany** → Berlin
-9. **Brazil** → Brasilia
-10. **Australia** → Canberra (Sydney in options)
-
-## Control Functions
-
-### Navigation Functions
-- **Next/Submit:** Move to next question or finish quiz
-- **Back:** Return to previous question (with score adjustment)
-- **Reset:** Restart quiz from the beginning
-- **Shuffle:** Randomize question order
-
-### Scoring Functions
-- **Real-time Tracking:** Score updates as questions are answered
-- **Answer History:** Maintains record of correct/incorrect responses
-- **Final Display:** Shows total score at quiz completion
-
-## How to Use
-1. **Start Quiz:** Launch the application to see the first question
-2. **Select Answer:** Click on one of the four radio button options
-3. **Navigate:** Use Next/Back buttons to move through questions
-4. **View Progress:** Current question number and score are tracked
-5. **Shuffle Questions:** Click Shuffle to randomize question order
-6. **Reset:** Start over at any time with the Reset button
-7. **Finish:** Complete all questions to see final score
-8. **Show Score:** View current score at any time
-
-## GUI Layout
-```
-┌─────────────────────────────────────┐
-│ Quiz Question │
-├─────────────────────────────────────┤
-│ ○ Option A │
-│ ○ Option B │
-│ ○ Option C │
-│ ○ Option D │
-├─────────────────────────────────────┤
-│ [Next] [Back] [Reset] [Shuffle] │
-│ [Show Score] │
-└─────────────────────────────────────┘
-```
+### 6. Wire the buttons
+```python title="quizapp.py"
+Button(root, text="Next", command=next_q).pack(side="left", padx=10, pady=20)
+Button(root, text="Shuffle", command=shuffle).pack(side="left", padx=10, pady=20)
+Button(root, text="Quit", command=root.destroy).pack(side="right", padx=10, pady=20)
-## Next Steps
-You can enhance this project by:
-- Adding more question categories (Science, History, Sports)
-- Implementing a timer for each question
-- Creating difficulty levels (Easy, Medium, Hard)
-- Adding images or multimedia to questions
-- Implementing user registration and profile saving
-- Creating a question bank with random selection
-- Adding detailed explanations for answers
-- Implementing multiple quiz modes (Timed, Survival, Practice)
-- Creating leaderboards and high scores
-- Adding sound effects and animations
-
-## Enhanced Features Ideas
-```python title="quizapp.py" showLineNumbers{1}
-def enhanced_quiz():
- # Features to add:
- # - Question categories and filtering
- # - Timer functionality
- # - Detailed result analysis
- # - User profiles and progress tracking
- # - Question difficulty ratings
- # - Multimedia support
- pass
-
-class QuizQuestion:
- def __init__(self, question, options, answer, category, difficulty):
- self.question = question
- self.options = options
- self.answer = answer
- self.category = category
- self.difficulty = difficulty
+root.mainloop()
```
-## Educational Applications
-- **Geography Learning:** World capitals and countries
-- **Academic Testing:** Create subject-specific quizzes
-- **Training Programs:** Employee knowledge assessment
-- **Language Learning:** Vocabulary and grammar tests
-- **Certification Prep:** Practice exams for various fields
-
-## Customization Options
-### Adding New Questions
-```python title="quizapp.py" showLineNumbers{1}
-new_questions = [
- "What is the largest planet?",
- "Who wrote Romeo and Juliet?",
- # Add more questions
+## Refactor: One Class Per Question
+
+Parallel arrays are an anti-pattern. The fix:
+```python title="oop.py"
+from dataclasses import dataclass
+
+@dataclass
+class Question:
+ text: str
+ options: list[str]
+ correct: int # index into options
+ explanation: str = "" # shown on the results screen
+
+BANK = [
+ Question("What is the capital of India?",
+ ["New Delhi", "Mumbai", "Kolkata", "Chennai"], 0,
+ "New Delhi has been India's capital since 1911."),
+ Question("What is the capital of USA?",
+ ["New York", "Washington DC", "Chicago", "Los Angeles"], 1,
+ "Washington, D.C. was founded as the capital in 1790."),
+ # …
]
+```
+Now each question owns its data. Shuffling is `random.shuffle(BANK)`. Adding an `explanation` to display at the end is a one-line addition.
-new_options = [
- ["Earth", "Jupiter", "Saturn", "Mars"],
- ["Shakespeare", "Dickens", "Austen", "Tolkien"],
- # Add corresponding options
-]
+## Load from JSON
-new_answers = [1, 0] # Index of correct answers
+```python title="bank.json"
+[
+ {"text": "Capital of India?", "options": ["New Delhi","Mumbai","Kolkata","Chennai"], "correct": 0, "explanation": "..."},
+ …
+]
+```
+```python title="load.py"
+import json
+from pathlib import Path
+BANK = [Question(**d) for d in json.loads(Path("bank.json").read_text())]
+```
+Non-programmers can now author quizzes by editing the JSON file. Multi-category support is one folder of JSON files.
+
+## Add a Per-Question Timer
+
+```python title="timer.py"
+remaining = 0
+def start_timer(seconds=15):
+ global remaining
+ remaining = seconds
+ tick()
+
+def tick():
+ timer_lbl.config(text=f"{remaining}s")
+ if remaining <= 0:
+ next_q() # auto-advance
+ return
+ root.after(1000, decrement)
+
+def decrement():
+ global remaining
+ remaining -= 1
+ tick()
+```
+Call `start_timer()` whenever `show_question` runs. The user has 15 seconds before the answer locks in.
+
+## Results Screen
+
+Instead of a popup, end on a result page that shows what they got right and wrong:
+```python title="results.py"
+def show_results():
+ win = Toplevel(root)
+ win.title("Results")
+ Label(win, text=f"Score: {score}/{len(BANK)}",
+ font=("Arial", 18, "bold")).pack(pady=10)
+ for i, q in enumerate(BANK):
+ correct = q.options[q.correct]
+ yours = q.options[answers_given[i]] if answers_given[i] >= 0 else "—"
+ mark = "✅" if answers_given[i] == q.correct else "❌"
+ Label(win, text=f"{mark} {q.text}\n You: {yours} Correct: {correct}\n {q.explanation}",
+ wraplength=600, justify="left").pack(anchor="w", padx=10)
```
+Educational quizzes live or die on the feedback — a results screen that explains wrong answers turns the quiz into a teaching tool.
-### Styling Improvements
-```python title="quizapp.py" showLineNumbers{1}
-# Enhanced styling options
-root.config(bg="#f0f0f0")
-question_font = ("Arial", 16, "bold")
-option_font = ("Arial", 12)
-button_style = {"font": ("Arial", 10), "bg": "#4CAF50", "fg": "white"}
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| Quiz hangs at last question | Off-by-one — `current > len` after submit | Check `>= len(questions)` |
+| Same question shown twice | Manually tracked index slipped | Use `enumerate` + a single source list |
+| Shuffle desyncs the answer | Shuffled `questions` but not `options` and `answers` | Use the `zip` + `shuffle` pattern, or `Question` objects |
+| `IntVar.get()` returns 0 with nothing selected | Default is 0, indistinguishable from "chose option 0" | Initialize to `-1` and treat as "unanswered" |
+| Timer fires after window closed | Pending `after` callback | Track and cancel with `root.after_cancel(id)` |
+| Score wrong after "Back" | Decrementing not implemented | Either disable Back, or recompute score from a list |
+
+## Variations to Try
+
+### 1. Multiple categories
+A category-picker screen. JSON folder structure:
+```
+banks/
+ geography.json
+ science.json
+ history.json
```
-## Educational Value
-This project teaches:
-- **GUI Development:** Creating interactive desktop applications
-- **Data Management:** Organizing questions, options, and answers
-- **Event Handling:** Managing user interactions and button clicks
-- **State Management:** Tracking quiz progress and scoring
-- **User Experience Design:** Creating intuitive interfaces
-
-## Common Enhancements
-- **Timer Implementation:** Add countdown for each question
-- **Category Selection:** Allow users to choose question topics
-- **Difficulty Levels:** Implement easy, medium, hard questions
-- **Results Analysis:** Show detailed performance statistics
-- **Data Persistence:** Save scores and progress to files
+### 2. Difficulty levels
+Add `difficulty: int` (1-5) to each `Question`. Pick a subset of questions matching the user's chosen level.
+
+### 3. Image questions
+Each `Question` can have an `image_path`. Display it with `tk.PhotoImage` above the text.
+
+### 4. Audio questions
+Useful for language quizzes — play a `.wav` clip and ask "what did the voice say?". See [Basic Music Player](./basicmusicplayer).
+
+### 5. Score persistence
+Save `(timestamp, category, score)` rows to a JSON file. Show a "high scores" leaderboard at startup.
+
+### 6. Online quiz bank
+Pull questions from [opentdb.com](https://opentdb.com) (free trivia API).
+
+### 7. Time per question
+Award more points for faster correct answers. Penalize wrong answers under time pressure to discourage random guessing.
+
+### 8. Adaptive difficulty
+After two correct answers in a row, jump up a difficulty level; after two wrong, drop down. Classic "spaced repetition" lite.
+
+### 9. Multiplayer
+Two players take turns on the same screen, or split-screen with separate scores.
+
+### 10. Quiz editor
+A second Tkinter window where instructors author new questions and save them to the JSON bank.
+
+### 11. Web version
+Flask + Jinja (see [Basic Web Server](./basicwebserver)). State lives in the session.
+
+### 12. Mobile version
+Re-implement in **Kivy** (or wrap the web version as a PWA) to ship to phones.
## Real-World Applications
-- **Educational Software:** Classroom quiz applications
-- **Training Systems:** Corporate knowledge testing
-- **Certification Tools:** Practice exams for licenses/certifications
-- **Entertainment:** Trivia games and brain teasers
-- **Assessment Tools:** Academic evaluation software
-
-## Performance Considerations
-- **Memory Usage:** Efficient data structure management
-- **Response Time:** Quick GUI updates and navigation
-- **Scalability:** Easy addition of new questions and categories
-- **User Experience:** Smooth interaction and feedback
+- **Education** — flashcard apps, language learning (Duolingo-style).
+- **Corporate training** — compliance and onboarding quizzes.
+- **Certification practice** — AWS / Cisco / Microsoft exam prep.
+- **Trivia games** — bar trivia apps, party games.
+- **Self-assessment** — personality, aptitude, skill measurement.
+
+## Educational Value
+- **Tkinter widgets** — Radiobutton + IntVar.
+- **State management** — current question, score, selected option.
+- **Why objects beat parallel arrays** — a textbook refactor.
+- **Persistence** — JSON-backed question banks.
+- **UX of feedback** — results screens vs. just a number.
+
+## Next Steps
+- Refactor to the **`Question` dataclass** above.
+- Move questions to a **JSON file**; support **multiple categories**.
+- Add a **per-question timer**.
+- Build a **results screen** with explanations.
+- Persist **high scores** to disk.
+- Try the **Open Trivia DB** integration for endless content.
## Conclusion
-In this project, we learned how to create a Basic Quiz Game using Python's Tkinter library. We explored GUI development, data management, event handling, and user interaction design. This project demonstrates how to create engaging educational software with proper navigation controls and scoring systems. The quiz application serves as an excellent foundation for more complex educational applications and can be easily customized for different subjects and learning objectives. To find more projects like this, you can visit Python Central Hub.
+You built a working quiz app, fixed its biggest weakness (parallel arrays), and have a path from "hard-coded ten capitals" to "multi-category, timed, leaderboard-driven trivia engine." The exact same architecture powers commercial flashcard apps and corporate training systems. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/quizapp.py). Find more games on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/randompasswordgenerator.mdx b/src/content/docs/projects/Beginners/randompasswordgenerator.mdx
index 0f430b2b..774d9753 100644
--- a/src/content/docs/projects/Beginners/randompasswordgenerator.mdx
+++ b/src/content/docs/projects/Beginners/randompasswordgenerator.mdx
@@ -1,6 +1,6 @@
---
title: Random Password Generator
-description: A Python application that generates strong, random passwords with customizable length using letters, numbers, and special characters.
+description: Generate strong random passwords in Python. Learn the difference between random and secrets, why most generators are insecure, password entropy, clipboard helpers, passphrase mode, and a Tkinter GUI version.
sidebar:
order: 16
hero:
@@ -13,148 +13,258 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Random Password Generator is a simple yet powerful Python application that creates strong, random passwords to help users maintain better security practices. The application generates passwords using a combination of uppercase letters, lowercase letters, numbers, and special characters. Users can specify the desired password length, making it flexible for different security requirements. This project demonstrates working with Python's `random` and `string` modules, user input handling, and string manipulation techniques.
+Most "password generator" tutorials online quietly teach you to write an insecure one. They use Python's `random` module — which is **not** cryptographically secure. In this tutorial we will build the basic version first (so you understand the parts), then fix it with the right module (`secrets`), then go further with entropy calculations, customization flags, clipboard copy, a passphrase mode, and a Tkinter GUI.
-## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
+You will learn:
+- Why `random` is wrong for passwords and `secrets` is right.
+- How to combine character sets (letters, digits, punctuation) cleanly.
+- How to **guarantee** that every required character type is present.
+- How to compute the **entropy** of a password and what that number means.
+- How to copy a generated password to the clipboard and clear it after a delay.
+- How to wrap the whole thing in a tiny GUI.
+
+## The `random` vs. `secrets` Trap
+
+Almost every online tutorial uses:
+```python title="insecure.py"
+import random
+password = ''.join(random.choice(charset) for _ in range(length)) # ❌
+```
+The `random` module uses the **Mersenne Twister** PRNG. It is fast, statistically uniform, and **completely predictable** if you know its state. Given enough output an attacker can reconstruct the seed and predict every future call.
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download).
+For anything security-related — passwords, tokens, session IDs, OTP codes — use **`secrets`**:
+```python title="secure.py"
+import secrets, string
+charset = string.ascii_letters + string.digits + string.punctuation
+password = ''.join(secrets.choice(charset) for _ in range(length)) # ✅
+```
+The `secrets` module is a thin wrapper over your OS's cryptographic randomness source (`/dev/urandom` on Unix, `CryptGenRandom` on Windows). It is what every standard-library example for cryptography uses.
-Note: This project uses only built-in Python modules (`random` and `string`), so no additional installations are required.
+## Prerequisites
+- Python 3.6 or above (`secrets` was added in 3.6).
+- A code editor or IDE.
+- Familiarity with strings, loops, and the `input()` function.
## Getting Started
-#### Create a Project
+
+### Create the project
1. Create a folder named `random-password-generator`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `randompasswordgenerator.py`.
-4. Copy the given code and paste it in your `randompasswordgenerator.py` file.
+2. Inside it, create `randompasswordgenerator.py`.
-## Write the Code
-1. Copy and paste the following code in your `randompasswordgenerator.py` file.
+### Write the code
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `random-password-generator`.
-```cmd title="command" showLineNumbers{1} {1-15}
-C:\Users\Your Name\random-password-generator> python randompasswordgenerator.py
-Random Password Generator
-Enter the length of password:
-12
-K8@mN2$pX9!z
+### Run it
+```cmd title="command" showLineNumbers{1} {1-15}
C:\Users\Your Name\random-password-generator> python randompasswordgenerator.py
Random Password Generator
-Enter the length of password:
-16
+Length: 16
aB3#dE7&hI9@kL2$
-
-C:\Users\Your Name\random-password-generator> python randompasswordgenerator.py
-Random Password Generator
-Enter the length of password:
-8
-mK9@nP2!
```
-## Explanation
-1. Import the required modules.
-```python title="randompasswordgenerator.py" showLineNumbers{1} {1-2}
-import random
+## Step-by-Step Explanation
+
+### 1. Pick the right module
+```python title="randompasswordgenerator.py"
+import secrets
import string
```
+- `secrets` for randomness.
+- `string` provides ready-made character classes (`ascii_letters`, `digits`, `punctuation`).
-2. Define the main password generation function.
-```python title="randompasswordgenerator.py" showLineNumbers{1} {1-3}
-def randompasswordgenerator():
- print("Random Password Generator")
- print("Enter the length of password: ")
+### 2. Build the character set
+```python title="randompasswordgenerator.py"
+ALPHABET = string.ascii_letters + string.digits + string.punctuation
```
+Concatenated strings — Python treats this as one big string of characters from which to pick.
-3. Get user input for password length.
-```python title="randompasswordgenerator.py" showLineNumbers{1} {1-2}
-length = int(input())
-password = ''
+### 3. Generate
+```python title="randompasswordgenerator.py"
+def generate(length: int = 16) -> str:
+ return "".join(secrets.choice(ALPHABET) for _ in range(length))
```
+A generator expression inside `"".join(...)` is the idiomatic way to build a string of N characters.
-4. Generate the password using random characters.
-```python title="randompasswordgenerator.py" showLineNumbers{1} {1-3}
-for i in range(length):
- password += random.choice(string.ascii_letters + string.digits + string.punctuation)
-print(password)
+### 4. Read the length and print
+```python title="randompasswordgenerator.py"
+length = int(input("Length: "))
+print(generate(length))
```
-5. Execute the function when script is run directly.
-```python title="randompasswordgenerator.py" showLineNumbers{1} {1-2}
-if __name__ == "__main__":
- randompasswordgenerator()
+## Guarantee Variety
+
+A naive generator may produce a password with no digits. Many password rules require at least one of each type:
+
+```python title="guarantee.py"
+import secrets, string
+
+def generate(length: int = 16) -> str:
+ if length < 4:
+ raise ValueError("length must be ≥ 4 to satisfy character-class rules")
+ pools = [
+ string.ascii_lowercase,
+ string.ascii_uppercase,
+ string.digits,
+ string.punctuation,
+ ]
+ # One mandatory character from each pool…
+ password = [secrets.choice(pool) for pool in pools]
+ # …then fill the rest from the combined alphabet
+ alphabet = "".join(pools)
+ password += [secrets.choice(alphabet) for _ in range(length - len(pools))]
+ secrets.SystemRandom().shuffle(password) # avoid predictable ordering
+ return "".join(password)
```
+Subtle point: do not just *append* one digit at the end — that is a known weakness. We **shuffle** the whole list so the required characters land in random positions.
+
+## Entropy: How Strong Is Strong?
-## Character Sets Used
-The password generator uses the following character sets from Python's `string` module:
+Password entropy in bits = `log₂(alphabet_size ** length)`.
-- **`string.ascii_letters`:** Both uppercase and lowercase letters (a-z, A-Z)
-- **`string.digits`:** Numbers (0-9)
-- **`string.punctuation`:** Special characters and symbols
+| Alphabet size | Length | Entropy (bits) | Comment |
+|---|---|---|---|
+| 26 (lowercase only) | 8 | ~38 | Crackable in minutes by a GPU |
+| 62 (letters + digits) | 12 | ~71 | Reasonable |
+| 95 (full printable ASCII) | 16 | ~104 | Strong |
+| 95 | 20 | ~131 | Excellent |
-This combination ensures strong passwords with high entropy and complexity.
+A function to compute it:
+```python title="entropy.py"
+import math
+def entropy_bits(password: str) -> float:
+ pool = 0
+ if any(c.islower() for c in password): pool += 26
+ if any(c.isupper() for c in password): pool += 26
+ if any(c.isdigit() for c in password): pool += 10
+ if any(c in string.punctuation for c in password): pool += len(string.punctuation)
+ return len(password) * math.log2(max(pool, 1))
-## Features
-- **Customizable Length:** Users can specify any password length
-- **Strong Character Mix:** Includes letters, numbers, and special characters
-- **High Randomness:** Uses Python's secure random module
-- **Simple Interface:** Easy-to-use command-line interface
-- **Instant Generation:** Fast password creation
-- **No External Dependencies:** Uses only built-in Python modules
+print(f"{entropy_bits(p):.1f} bits")
+```
+The conventional wisdom: aim for **≥ 80 bits** for important accounts, **≥ 128 bits** for master passwords / vault keys.
-## Security Considerations
-- **Character Variety:** Uses all types of characters for maximum security
-- **Randomness:** Each character is independently randomly selected
-- **Length Flexibility:** Supports any length for different security requirements
-- **No Patterns:** Generated passwords have no predictable patterns
+## Avoid Ambiguous Characters
-## Password Strength Guidelines
-- **Minimum 8 characters:** For basic security
-- **12+ characters:** For good security
-- **16+ characters:** For excellent security
-- **Include all character types:** Letters, numbers, and symbols
+Some characters are easy to confuse: `0` / `O`, `1` / `l` / `I`. Let the user opt out:
+```python title="no_ambig.py"
+AMBIGUOUS = "0Oo1lI"
+def generate(length, no_ambig=False):
+ pool = ALPHABET
+ if no_ambig:
+ pool = "".join(c for c in pool if c not in AMBIGUOUS)
+ return "".join(secrets.choice(pool) for _ in range(length))
+```
-## Next Steps
-You can enhance this project by:
-- Adding a GUI interface using Tkinter
-- Implementing password strength indicators
-- Adding options to exclude similar characters (0, O, l, 1)
-- Creating memorable password options (using words + numbers)
-- Adding password history (with secure storage)
-- Implementing copy-to-clipboard functionality
-- Adding batch password generation
-- Creating password validation against common passwords
-- Adding custom character set options
-- Implementing pronunciation guides for generated passwords
-
-## Enhanced Version Ideas
-```python title="randompasswordgenerator.py" showLineNumbers{1}
-def enhanced_password_generator():
- # Add options for:
- # - Exclude ambiguous characters
- # - Ensure at least one of each character type
- # - Generate multiple passwords at once
- # - Save passwords to encrypted file
- pass
+## Copy to Clipboard
+
+Generated passwords are easier to use if they land directly in the clipboard. Two options:
+
+```bash title="install"
+pip install pyperclip
+```
+```python title="clipboard.py"
+import pyperclip
+pyperclip.copy(password)
+print("Copied to clipboard.")
+```
+
+For extra hygiene, clear the clipboard after 30 seconds:
+```python title="clear.py"
+import threading
+def clear_later():
+ pyperclip.copy("")
+threading.Timer(30, clear_later).start()
+```
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| Uses `random.choice` | Insecure PRNG | Switch to `secrets.choice` |
+| Required character placed at index 0 every time | Built the password by *appending* required characters | Shuffle after assembly |
+| `int(input())` crashes on letters | No validation | Wrap with `try/except` |
+| Password always rejected by the site | Missing a required character class | Use the "guarantee variety" version |
+| Same password generated twice in a script | Reused a seeded `random` | Never seed `secrets` (it is not seedable) |
+
+## Variations to Try
+
+### 1. Passphrase mode (Diceware)
+Long, memorable, and high-entropy. Five random words from a word list of 7,776 is ~64 bits already.
+```python title="passphrase.py"
+import secrets, pathlib
+WORDS = pathlib.Path("eff_wordlist.txt").read_text().splitlines()
+def passphrase(n=5):
+ return "-".join(secrets.choice(WORDS) for _ in range(n))
+```
+EFF publishes a free word list specifically for this purpose.
+
+### 2. Multiple passwords at once
+```python title="batch.py"
+n = int(input("How many? "))
+for _ in range(n):
+ print(generate(16))
```
+### 3. Strength meter
+Reuse the entropy function above; rate as Weak / OK / Strong / Excellent.
+
+### 4. CLI flags with argparse
+```bash title="cli"
+python randompasswordgenerator.py --length 20 --no-ambig --count 5
+```
+
+### 5. GUI version
+```python title="gui.py"
+import tkinter as tk
+from tkinter import ttk
+def gen():
+ out_var.set(generate(int(length_var.get())))
+root = tk.Tk()
+length_var = tk.StringVar(value="16")
+out_var = tk.StringVar()
+tk.Entry(root, textvariable=length_var).pack()
+tk.Button(root, text="Generate", command=gen).pack()
+tk.Entry(root, textvariable=out_var, width=40).pack()
+root.mainloop()
+```
+
+### 6. Save to an encrypted vault
+Append generated passwords (with a label) to an encrypted file using `cryptography.fernet`. See [Personal Diary](./personaldiary) for the same encryption idea.
+
+### 7. QR-code output
+Use `pip install qrcode` to render the password as a QR code you can scan to a phone.
+
+### 8. Check against breach corpora
+Send a SHA-1 prefix of the password to **Have I Been Pwned**'s [k-anonymity API](https://haveibeenpwned.com/API/v3#PwnedPasswords) — if it appears in known breaches, regenerate.
+
+## Best Practices for Password Generation
+- **Use `secrets`.** Always. Forever.
+- **Length matters more than complexity.** A long passphrase beats a short symbol soup.
+- **Guarantee character classes** when sites demand them; do not hope randomness gives you one.
+- **Shuffle after assembly** to avoid predictable positions.
+- **Never log generated passwords** anywhere (stdout in a CI run, error message, debug print).
+- **Copy via clipboard, do not save to a plain file.** If you must persist, encrypt.
+
## Common Use Cases
-- **User Account Creation:** Generate strong passwords for new accounts
-- **Password Updates:** Create new passwords for existing accounts
-- **Temporary Passwords:** Generate one-time use passwords
-- **API Keys:** Create random strings for API authentication
-- **Security Testing:** Generate test passwords for applications
-
-## Best Practices
-- **Never Reuse:** Generate a unique password for each account
-- **Store Securely:** Use a password manager to store generated passwords
-- **Regular Updates:** Change passwords periodically
-- **Avoid Predictable Patterns:** Use random generation over patterns
-- **Backup Safely:** Keep secure backups of important passwords
+- New account signups.
+- API key / token generation.
+- One-time temporary passwords for password resets.
+- Database / service credentials (developer ergonomics + safety).
+- Wi-Fi guest passwords printed on a slip.
+
+## Educational Value
+- **Cryptographic randomness** — why `random` is the wrong tool.
+- **Entropy** — turning intuition about password strength into a number.
+- **String composition** — building from character classes.
+- **CLI ergonomics** — flags, validation, clipboard integration.
+- **Security mindset** — small choices like "shuffle after assembly" matter.
+
+## Next Steps
+- Switch to `secrets` everywhere immediately.
+- Add **clipboard copy** with auto-clear.
+- Add a **passphrase mode** using the EFF word list.
+- Integrate with **Have I Been Pwned** for compromise checks.
+- Wrap in a **GUI** or build it into a real **password manager** (encrypted vault, master password, search).
## Conclusion
-In this project, we learned how to create a Random Password Generator using Python's built-in modules. We explored the `random` module for generating random selections and the `string` module for accessing different character sets. This project demonstrates essential security programming concepts and provides a practical tool for everyday use. The simplicity of the implementation makes it an excellent starting point for more advanced security-related projects. To find more projects like this, you can visit Python Central Hub.
+You generated random passwords, then learned why "random" is the wrong word — and rebuilt the generator with `secrets`. You measured entropy, guaranteed variety, copied to the clipboard, and have a roadmap to a real password manager. Security is full of these small, mostly-invisible choices. The full source is on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/randompasswordgenerator.py). Find more security-focused projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/rockpaperscissors.mdx b/src/content/docs/projects/Beginners/rockpaperscissors.mdx
index d95e157d..826cce3f 100644
--- a/src/content/docs/projects/Beginners/rockpaperscissors.mdx
+++ b/src/content/docs/projects/Beginners/rockpaperscissors.mdx
@@ -1,6 +1,6 @@
---
title: Rock Paper Scissors Game
-description: This is a simple rock paper scissors game made using python. This is a beginner level project. You can find the code for this project on my GitHub. In this project, we will use the random module to generate random numbers and then we will use if-else statements to check the user input and computer generated number and then we will decide the winner.
+description: Build a Rock Paper Scissors game in Python against the computer. Learn random choice, dictionary-driven rules, score tracking, AI strategies, and how to grow a 20-line script into a polished game.
sidebar:
order: 6
hero:
@@ -13,30 +13,51 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Rock, Paper, Scissors is a hand game played between two people. The game is played by two people using their hands. The players count to three in unison and simultaneously "throw" one of three hand signals that correspond to rock, paper or scissors. The winner is determined by the hand signals thrown by the players. The rules of the game are:
-- Rock wins against scissors.
-- Scissors win against paper.
-- Paper wins against rock.
-- If both players throw the same hand signal, there is no winner and the game is played again.
+Rock Paper Scissors is one of the oldest decision games still in play — and one of the best for teaching beginners how to model rules in code. In this project you will build a command-line version of the game where you play against the computer. Along the way you will learn how to pick a random item from a list, validate user input, model game rules cleanly (avoiding huge `if`/`elif` chains), and grow the program with score keeping, replay, and even a basic AI opponent.
-We are going to make a simple rock paper scissors game using python. This is a beginner level project. You can find the code for this project on my GitHub. In this project, we will use the random module to generate random numbers and then we will use if-else statements to check the user input and computer generated number and then we will decide the winner. You are against the computer in this game. The computer will randomly choose between rock, paper and scissors. You will be asked to choose between rock, paper and scissors. The computer will then check your choice against its choice and decide the winner. The game will continue until you decide to quit.
+You will leave this tutorial comfortable with:
+- `random.choice()` for picking from a list.
+- Loops with input validation.
+- Dictionary-driven rules instead of chained conditionals.
+- Counting wins / losses / ties across rounds.
+- The Markov-chain trick that lets an AI start beating humans.
+
+## The Rules
+- **Rock** crushes **Scissors**.
+- **Scissors** cut **Paper**.
+- **Paper** covers **Rock**.
+- Same choice → tie.
+
+That is the entire game. The challenge is encoding those rules so the program is readable and easy to extend (Rock-Paper-Scissors-Lizard-Spock anyone?).
## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
+- Python 3.6 or above.
+- A text editor or IDE (VS Code recommended).
+- Comfort running a `.py` file from the terminal.
+- Familiarity with `if`/`elif`/`else` and `input()`.
+
+## Concepts You Will Use
+
+| Concept | Purpose |
+|---|---|
+| **`random.choice(list)`** | Pick a random element from a sequence. |
+| **`.upper()` / `.lower()`** | Normalize user input so case does not matter. |
+| **`while True` + `break`** | Replay loop until the user quits. |
+| **Dictionary lookups** | Replace long `if`/`elif` chains with a clean data structure. |
+| **F-strings** | Format output cleanly. |
## Getting Started
-#### Create a Project
-1. Create a new folder and name it `rockpaperscissorsgame`.
-2. Open the folder in your code editor.
-3. Create a new file and name it `rockpaperscissors.py`.
-4. Copy the code given at the beginning of this article and paste it in the `rockpaperscissors.py` file.
-
-## Write the Code
-1. Copy and paste the following code in the `rockpaperscissors.py` file.
+
+### Create the project
+1. Make a folder named `rockpaperscissors-game`.
+2. Inside it, create `rockpaperscissors.py`.
+3. Open the folder in your editor.
+
+### Write the code
-2. Save the File.
-3. Open the terminal and navigate to the `rockpaperscissorsgame` folder.
+
+Save the file, open a terminal in the folder, run:
+
```cmd title="command" showLineNumbers{1} {1}
C:\Users\username\PythonCentralHub\projects\beginners\rockpaperscissorsgame> python rockpaperscissors.py
Choose Rock, Paper or Scissors: Rock
@@ -44,88 +65,188 @@ Computer chose SCISSORS. You win!
Do you want to play again? (y/n): y
Choose Rock, Paper or Scissors: paper
Computer chose PAPER. It's a tie!
-Do you want to play again? (y/n): y
-Choose Rock, Paper or Scissors: scissors
-Computer chose SCISSORS. It's a tie!
Do you want to play again? (y/n): n
Thanks for playing!
```
-4. You can see that the game is working as expected. You can play the game as many times as you want. You can also quit the game whenever you want.
-## Explanation
-1. We first import the `random` module. We will use this module to generate random numbers.
-```python title="rockpaperscissors.py" showLineNumbers{1} {1}
+## Step-by-Step Explanation
+
+### 1. Import randomness
+```python title="rockpaperscissors.py"
import random
```
+`random.choice([...])` picks one item from a list uniformly at random.
-2. We create a list of options. The computer will randomly choose from this list.
-```python title="rockpaperscissors.py" showLineNumbers{1} {1}
+### 2. Build the option list
+```python title="rockpaperscissors.py"
options = ["ROCK", "PAPER", "SCISSORS"]
```
+Capitalize all entries up front. Then normalize the user's input to uppercase too — comparisons become trivial.
-3. We create a function to play the game. This function will be called whenever we want to play the game.
-```python title="rockpaperscissors.py" showLineNumbers{1} {1}
-def play():
-```
-
-4. We get the user's choice. We use the `input()` function to get the user's choice. We convert the user's choice to uppercase using the `upper()` function. We store the user's choice in the `user_choice` variable.
-```python title="rockpaperscissors.py" showLineNumbers{1} {1}
+### 3. Read the player's choice
+```python title="rockpaperscissors.py"
user_choice = input("Choose Rock, Paper or Scissors: ").upper()
+while user_choice not in options:
+ user_choice = input("Invalid input. Choose Rock, Paper or Scissors: ").upper()
```
+The `while` loop is a **validation loop**. It refuses to move on until the user types something valid. No `try/except` needed because we are only checking membership in a list.
-5. We get the computer's choice. We use the `random.choice()` function to get the computer's choice. We pass the `options` list as an argument to the `random.choice()` function. We store the computer's choice in the `computer_choice` variable.
-```python title="rockpaperscissors.py" showLineNumbers{1} {1}
+### 4. Computer picks a choice
+```python title="rockpaperscissors.py"
computer_choice = random.choice(options)
```
-6. We check if the user's choice is valid. We use the `while` loop to check if the user's choice is valid. We check if the user's choice is in the `options` list. If the user's choice is not in the `options` list, we ask the user to enter a valid choice. We convert the user's choice to uppercase using the `upper()` function. We store the user's choice in the `user_choice` variable.
-```python title="rockpaperscissors.py" showLineNumbers{1} {1}
-while user_choice not in options:
- user_choice = input("Invalid input. Choose Rock, Paper or Scissors: ").upper()
-```
-
-7. We check the user's choice against the computer's choice. We use the `if-elif-else` statements to check the user's choice against the computer's choice. We convert the user's choice to uppercase using the `upper()` function. We convert the computer's choice to uppercase using the `upper()` function. If the user's choice is equal to the computer's choice, we print that it's a tie. If the user's choice is rock and the computer's choice is scissors, we print that the user wins. If the user's choice is paper and the computer's choice is rock, we print that the user wins. If the user's choice is scissors and the computer's choice is paper, we print that the user wins. If the user's choice is not equal to the computer's choice, we print that the user loses.
-```python title="rockpaperscissors.py" showLineNumbers{1} {1-20}
-if user_choice.upper() == computer_choice.upper():
+### 5. Decide the winner
+```python title="rockpaperscissors.py"
+if user_choice == computer_choice:
print(f"Computer chose {computer_choice}. It's a tie!")
-elif user_choice.upper() == "ROCK" and computer_choice.upper() == "SCISSORS":
- print(f"Computer chose {computer_choice}. You win!")
-elif user_choice.upper() == "PAPER" and computer_choice.upper() == "ROCK":
- print(f"Computer chose {computer_choice}. You win!")
-elif user_choice.upper() == "SCISSORS" and computer_choice.upper() == "PAPER":
+elif (user_choice == "ROCK" and computer_choice == "SCISSORS") \
+ or (user_choice == "PAPER" and computer_choice == "ROCK") \
+ or (user_choice == "SCISSORS" and computer_choice == "PAPER"):
print(f"Computer chose {computer_choice}. You win!")
else:
print(f"Computer chose {computer_choice}. You lose!")
```
+That `elif` is doing a lot. The next section shows a cleaner way.
-8. We play the game. We use the `while` loop to play the game. We call the `play()` function to play the game. We ask the user if they want to play again. If the user enters `y`, we play the game again. If the user enters `n`, we break out of the `while` loop.
-```python title="rockpaperscissors.py" showLineNumbers{1} {1-8}
+### 6. Replay loop
+```python title="rockpaperscissors.py"
while True:
play()
- play_again = input("Do you want to play again? (y/n): ")
- if play_again.lower() != "y":
+ if input("Do you want to play again? (y/n): ").lower() != "y":
+ print("Thanks for playing!")
break
```
-9. We print a message to thank the user for playing the game.
-```python title="rockpaperscissors.py" showLineNumbers{1} {1}
-print("Thanks for playing!")
+## Cleaner Rules with a Dictionary
+
+A growing `elif` chain is a smell. Replace it with a dictionary that maps each choice to "what it beats":
+
+```python title="cleaner_rules.py"
+BEATS = {
+ "ROCK": "SCISSORS",
+ "SCISSORS": "PAPER",
+ "PAPER": "ROCK",
+}
+
+def winner(user, computer):
+ if user == computer:
+ return "tie"
+ return "user" if BEATS[user] == computer else "computer"
+```
+Reading top to bottom: *"Rock beats Scissors. Scissors beat Paper. Paper beats Rock."* Adding a new option later (e.g., Lizard, Spock) means **one** more line per option, not a combinatorial explosion of `elif`s.
+
+### Bonus: Rock-Paper-Scissors-Lizard-Spock
+```python title="rpsls.py"
+BEATS = {
+ "ROCK": ["SCISSORS", "LIZARD"],
+ "PAPER": ["ROCK", "SPOCK"],
+ "SCISSORS": ["PAPER", "LIZARD"],
+ "LIZARD": ["PAPER", "SPOCK"],
+ "SPOCK": ["SCISSORS", "ROCK"],
+}
+
+def winner(user, computer):
+ if user == computer:
+ return "tie"
+ return "user" if computer in BEATS[user] else "computer"
```
+Five-option variant from *The Big Bang Theory*. Same code shape, more fun.
+
+## Add Score Tracking
+
+```python title="score.py"
+score = {"user": 0, "computer": 0, "tie": 0}
+
+def play_round():
+ # … existing logic …
+ result = winner(user_choice, computer_choice)
+ score[result] += 1
+ print(f"Score — You: {score['user']} Computer: {score['computer']} Ties: {score['tie']}")
+```
+
+After each round you see the running totals. When the user quits, print a final summary.
+
+## A Smarter Computer
+
+Random play means the computer wins 33 % of the time. Humans have patterns. A simple **Markov-chain AI** tracks what the player typed last round and bets on the same again:
+
+```python title="ai.py"
+COUNTER = {"ROCK": "PAPER", "PAPER": "SCISSORS", "SCISSORS": "ROCK"}
+last_user_move = None
+
+def ai_pick():
+ global last_user_move
+ if last_user_move is None:
+ return random.choice(options)
+ return COUNTER[last_user_move] # counter the player's previous move
+
+# after the round:
+last_user_move = user_choice
+```
+Naive but surprisingly effective — most beginners *do* repeat their previous move. For a real upgrade, track the **transition matrix** (what move follows what) and predict accordingly.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| User typing `rock` is rejected | Forgot `.upper()` | Normalize both sides to one case |
+| Always loses to Scissors | Typo in the rules table | Compare against a single dictionary |
+| Loop never exits | Wrong indentation of `break` | Make sure `break` is inside the `if` |
+| Score persists between runs but should reset | Globals are reused on replay | Reset score inside the replay loop |
+
+## Variations to Try
+
+### 1. Best of N rounds
+First to 3 wins takes the match:
+```python title="bo3.py"
+while score["user"] < 3 and score["computer"] < 3:
+ play_round()
+print("You won the match!" if score["user"] == 3 else "Computer wins the match.")
+```
+
+### 2. Two human players
+Ask each player privately for their move (clear the terminal between turns).
+
+### 3. Animated countdown
+Use `time.sleep` and `print("...", end="\r")` to print *"Rock... Paper... Scissors!"*.
+
+### 4. GUI version
+Tkinter with three big buttons. The label shows the result.
+
+### 5. Network play
+Build a simple Flask backend (see [Basic Web Server](./basicwebserver)) so two players in different places submit moves and the server decides the winner.
+
+### 6. Hand-gesture version
+Use OpenCV to detect the user's hand pose with a webcam. See [Gesture Recognition System](../Advance/gesture_recognition_system) for the techniques.
+
+### 7. Statistics dashboard
+After 100 rounds, plot the user's move distribution. Are you secretly a "rock-loving" player?
+
+## Real-World Applications
+- Tutorial for finite-state game logic.
+- Demonstrations of probability and game-theory mixed strategies.
+- Onboarding example for new game programmers.
+- A great first project for teaching simple AI / pattern detection.
+
+## Best Practices Demonstrated
+- **Normalize input early** so all comparisons assume one form.
+- **Encode rules as data** (a dictionary) instead of code (a chain of `if`s).
+- **Separate the round from the match** — `play_round()` and `play_match()` should be different functions.
+- **Track state explicitly** — score in a dictionary, not three scattered variables.
+
+## Educational Value
+This project teaches:
+- **Game loops** — round, match, replay.
+- **Data-driven rules** — the heart of every config-driven system.
+- **Input validation** — the `while not valid` pattern.
+- **Random vs. deterministic AI** — a fast intro to game-playing agents.
## Next Steps
-Congratulations on completing this project. You can now move on to the next project where we will make a simple calculator using python. You can find the code for this project on my GitHub. If you like this article, please share it with your friends and colleagues. If you have any questions or suggestions, please let me know in the comments below.
-
-There are some suggestions:
-- Add a score counter to keep track of the score.
-- Add a timer to limit the time for each round.
-- Add a GUI to make the game more interactive.
-- Add more options to make the game more interesting.
-- Add a database to store the scores.
-- Add a leaderboard to display the top scores.
-- Add a multiplayer mode to play with friends.
-- Add a chat feature to chat with friends while playing the game.
-- Add a voice assistant to make the game more interactive.
-- Add a virtual assistant to play the game with you.
+- Add **score-based difficulty**: after 5 losses, the AI eases off.
+- Implement **tournament mode** with brackets.
+- Wrap state in a `Game` class to learn OOP. See the [Hangman](./hangman) project for a similar refactor.
+- Build a **web version** that two players in different browsers can join.
## Conclusion
-In this article, we learned how to make a simple rock paper scissors game using python. This is a beginner level project. You can find the code for this project on my GitHub. In this project, we used the random module to generate random numbers and then we used if-else statements to check the user input and computer generated number and then we decided the winner. You can create more projects using python. You can find more projects on Python Central Hub. If you have any questions or suggestions, please let me know in the comments below.
\ No newline at end of file
+You wrote a complete game, refactored its rules into a clean data structure, added score keeping, and even gave the computer a tiny brain. The same patterns — validation loops, dictionary-driven rules, score state — turn up in every game and many non-game projects. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/rockpaperscissors.py). Find more beginner projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/rssfeedreader.mdx b/src/content/docs/projects/Beginners/rssfeedreader.mdx
index 2abc1515..0808f0ca 100644
--- a/src/content/docs/projects/Beginners/rssfeedreader.mdx
+++ b/src/content/docs/projects/Beginners/rssfeedreader.mdx
@@ -1,6 +1,6 @@
---
title: RSS Feed Reader
-description: Create an RSS feed reader that parses and displays news feeds with automatic browser opening
+description: Build a Python RSS reader that fetches, parses, and displays articles from any RSS or Atom feed. Learn the feedparser library, working with structured XML data, and how to extend a console reader into a full-featured news aggregator.
sidebar:
order: 51
hero:
@@ -14,104 +14,258 @@ import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Build a functional RSS feed reader that fetches content from RSS feeds, parses the data, and displays article information including titles, summaries, publication dates, and authors. The application can automatically open articles in a web browser for detailed reading.
+RSS (Really Simple Syndication) is the oldest still-working standard for publishing a list of articles in a machine-readable format. Most blogs, news sites, podcast hosts, and YouTube channels expose an RSS or Atom feed. In this project you will build a Python program that fetches a feed, parses it, and displays each article's title, summary, publication date, author, and tags — then optionally opens any article in your default browser.
-## Prerequisites
+This project teaches:
+- How HTTP and feed formats fit together.
+- How to use the `feedparser` library to handle the messy real-world variations of RSS and Atom.
+- How to extract structured data and present it nicely.
+- How to extend a simple reader into a personalized news aggregator.
-- Python 3.6 or above
-- Text Editor or IDE
-- Basic understanding of Python syntax
-- Knowledge of HTTP requests and web data
-- Familiarity with XML/RSS feed structures
-- Understanding of web scraping concepts
+## What Is RSS?
-## Getting Started
+An RSS feed is a single XML document served from a URL. It contains:
+- **Feed-level metadata** — title, description, link to the website, last-updated date.
+- **A list of entries** — each one with its own title, link, summary or content, published date, author, and categories (tags).
+
+When you "subscribe to" a feed, your reader app fetches that URL on a schedule and shows you new entries. There is no signup, no algorithm, no tracking — just a URL.
+
+Feeds typically live at URLs like:
+- `https://news.ycombinator.com/rss`
+- `https://www.reddit.com/r/Python/.rss`
+- `https://feeds.bbci.co.uk/news/rss.xml`
+- `https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID`
+
+## Prerequisites
+
+- Python 3.6 or above.
+- A text editor or IDE.
+- An internet connection.
+- Comfort installing packages with `pip`.
-### Create a new project
-1. Create a new project folder and name it `rssFeedReader`.
-2. Create a new file and name it `rssfeedreader.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into your `rssfeedreader.py` file.
+## Install Dependencies
-### Install required dependencies
-1. Install feedparser library:
-```bash
+```bash title="install"
pip install feedparser
```
+`feedparser` is the standard Python library for reading both RSS and Atom (a slightly different, newer format). It hides the differences between formats so you only deal with a uniform structure.
+
+## Getting Started
+
+### Create the project
+1. Create a folder named `rss-feed-reader`.
+2. Inside it, create `rssfeedreader.py`.
+3. Open the folder in your editor.
+
### Write the code
-1. Add the following code to your `rssfeedreader.py` file.
-2. Save the file.
-3. Run the following command to run the application.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\rssFeedReader> python rssfeedreader.py
+
+### Run it
+```bash title="run"
+python rssfeedreader.py
+```
+Sample output:
+```
Feed Title: Python - Reddit
Article 1: How to build a web scraper with Python
-Summary: Learn the basics of web scraping...
+Summary: Learn the basics of web scraping using BeautifulSoup...
Published: Mon, 01 Sep 2025 10:30:00 GMT
Author: pythondev123
Tags: programming, python, web-scraping
-[Article opens in browser]
```
- ```bash
- pip install feedparser
- ```
+## Step-by-Step Explanation
+
+### 1. Import what you need
+```python title="rssfeedreader.py"
+import feedparser
+import webbrowser
```
+- `feedparser` does all the HTTP + XML work.
+- `webbrowser` opens URLs in the user's default browser with one call.
-## Explanation
+### 2. Fetch and parse the feed
+```python title="rssfeedreader.py"
+url = "https://www.reddit.com/r/Python/.rss"
+feed = feedparser.parse(url)
+```
+A single call. Under the hood `feedparser`:
+1. Sends an HTTP `GET` to the URL.
+2. Reads the response body.
+3. Parses it as XML.
+4. Normalizes the differences between RSS 1.0, RSS 2.0, and Atom into a common object structure.
-1. The `import feedparser` statement imports the feedparser library for RSS/Atom feed parsing.
-2. The `import webbrowser` statement imports the webbrowser module for opening URLs.
-3. The `url = 'https://www.reddit.com/r/Python/.rss'` sets the RSS feed URL to parse.
-4. The `feed = feedparser.parse(url)` fetches and parses the RSS feed data.
-5. The `feed['feed']['title']` extracts the main feed title information.
-6. The `for entry in feed['entries']:` loop iterates through all articles in the feed.
-7. The `entry['title']` displays individual article titles.
-8. The `entry['summary']` shows article summaries and descriptions.
-9. The `entry['published']` displays publication dates and timestamps.
-10. The `entry['author']` shows article author information.
-11. The `webbrowser.open(entry['link'])` opens articles in the default browser.
-12. The `entry['tags']` displays article categories and tags.
+The result is an object that *looks like* a dictionary. You can also access values with dot notation: `feed.feed.title`.
-## Next Steps
+### 3. Read feed-level metadata
+```python title="rssfeedreader.py"
+print("Feed Title:", feed["feed"]["title"])
+print("Feed Link:", feed["feed"]["link"])
+print("Feed Description:", feed["feed"].get("description", "—"))
+```
+Note `feed["feed"]` — yes, the feed object has a `feed` key for the *feed-level* fields, separate from the list of entries. It is a quirk of `feedparser`'s API.
-Congratulations! You have successfully created an RSS Feed Reader in Python. Experiment with the code and see if you can modify the application. Here are a few suggestions:
-- Add GUI interface with Tkinter for better user experience
-- Implement feed subscription management
-- Create article bookmarking and saving features
-- Add search and filtering capabilities
-- Implement offline reading with article caching
-- Create notification system for new articles
-- Add multiple feed sources support
-- Implement article sorting by date or popularity
-- Create custom feed categories and organization
+Using `.get(key, default)` avoids `KeyError` when a feed omits an optional field.
-## Conclusion
+### 4. Loop through entries
+```python title="rssfeedreader.py"
+for i, entry in enumerate(feed.entries, start=1):
+ print(f"\nArticle {i}: {entry.title}")
+ print(f"Summary: {entry.get('summary', '')}")
+ print(f"Published: {entry.get('published', 'unknown')}")
+ print(f"Author: {entry.get('author', 'unknown')}")
+ tags = [t.term for t in entry.get('tags', [])]
+ if tags:
+ print(f"Tags: {', '.join(tags)}")
+```
+- `enumerate(..., start=1)` gives you both the index (starting at 1) and the entry.
+- Each entry has at least `title` and `link`. Everything else is optional and varies by source.
+- Tags arrive as a list of objects; `t.term` is the actual tag string.
+
+### 5. Open articles in the browser
+```python title="rssfeedreader.py"
+choice = input("\nEnter article number to open (or press Enter to skip): ")
+if choice.strip().isdigit():
+ n = int(choice) - 1
+ if 0 <= n < len(feed.entries):
+ webbrowser.open(feed.entries[n].link)
+```
+`webbrowser.open(url)` launches your default browser pointed at that URL. Cross-platform with no setup.
-In this project, you learned how to create an RSS Feed Reader in Python using the feedparser library. You also learned about web data parsing, XML processing, and browser integration. You can find the source code on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/rssfeedreader.py)
-- Add multiple feed aggregation and comparison
-- Include article rating and favorites system
+## Cleaning Up HTML in Summaries
+
+Many feeds put HTML in the summary field — paragraphs, links, embedded images. To show plain text, strip the tags:
+
+```python title="strip_html.py"
+import re
+
+def strip_html(text):
+ return re.sub(r"<[^>]+>", "", text or "").strip()
+
+print(strip_html(entry.summary))
+```
+
+For higher-quality output, use **BeautifulSoup**:
+```python title="bs4_clean.py"
+from bs4 import BeautifulSoup
+text = BeautifulSoup(entry.summary, "html.parser").get_text()
+```
-### Learning Extensions
-- Study RSS/Atom feed specifications and standards
-- Explore web scraping for non-RSS content sources
-- Learn about content aggregation algorithms
-- Practice with database integration for article storage
-- Understand feed validation and error handling
-- Explore real-time feed monitoring techniques
+## Handling Network Errors
+
+`feedparser` does not raise on bad URLs; it sets a status. Always check it:
+
+```python title="check_status.py"
+feed = feedparser.parse(url)
+if feed.bozo: # bozo flag = something went wrong
+ print(f"Warning: feed may be malformed — {feed.bozo_exception}")
+if not feed.entries:
+ print("No entries found.")
+```
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `KeyError: 'author'` | Feed omits an optional field | Use `entry.get('author', 'unknown')` |
+| `UnicodeEncodeError` printing entries | Terminal cannot render some characters | Set `PYTHONIOENCODING=utf-8` or pipe to a file |
+| Empty `feed.entries` | URL is HTML, not a feed; or site blocks the user agent | Pass a custom `User-Agent` via `feedparser.parse(url, request_headers={...})` |
+| `webbrowser.open` does nothing | Running in a headless environment | Print the URL instead |
+
+## Variations to Try
+
+### 1. Read multiple feeds
+```python title="multi_feed.py"
+FEEDS = [
+ "https://news.ycombinator.com/rss",
+ "https://www.reddit.com/r/Python/.rss",
+ "https://feeds.bbci.co.uk/news/technology/rss.xml",
+]
+for url in FEEDS:
+ feed = feedparser.parse(url)
+ print(f"\n=== {feed.feed.title} ===")
+ for entry in feed.entries[:5]:
+ print(f" • {entry.title}")
+```
+
+### 2. Sort entries by date
+Use the parsed `published_parsed` field, which is a Python `struct_time`:
+```python title="sort.py"
+import time
+entries = sorted(feed.entries,
+ key=lambda e: e.get("published_parsed", time.gmtime(0)),
+ reverse=True)
+```
+
+### 3. Save articles offline
+Write each entry's text content to a file for offline reading:
+```python title="offline.py"
+from pathlib import Path, PurePath
+safe = "".join(c for c in entry.title if c.isalnum() or c == " ")[:80]
+Path(f"articles/{safe}.txt").write_text(entry.summary, encoding="utf-8")
+```
+
+### 4. Show only new articles (since last run)
+Store the timestamp of the last fetch in a file. Filter entries newer than that:
+```python title="incremental.py"
+last_run = float(Path("last_run.txt").read_text() or 0)
+new_entries = [e for e in feed.entries
+ if time.mktime(e.published_parsed) > last_run]
+Path("last_run.txt").write_text(str(time.time()))
+```
+
+### 5. Send a daily email digest
+Combine `feedparser` with `smtplib` (Python's email module) to mail yourself the new entries each morning.
+
+### 6. Build a GUI reader
+Wrap the loop in a Tkinter app with a feed list on the left and articles on the right. See [Simple Blog System](./simple-blog-system) for the layout pattern.
+
+### 7. Convert to OPML
+OPML is the standard format readers use to import/export feed subscriptions. Generate one from your list of URLs.
+
+### 8. Search across feeds
+After fetching, store all entries in a list and let the user filter by keyword:
+```python title="search.py"
+keyword = input("Search: ").lower()
+matches = [e for e in all_entries if keyword in e.title.lower()]
+```
+
+## Best Practices
+
+- **Cache responses.** Hitting the same feed every few seconds is rude. Honor the feed's `etag` and `modified` headers and pass them on subsequent requests so the server can answer with `304 Not Modified`.
+ ```python title="cache.py"
+ feed = feedparser.parse(url, etag=last_etag, modified=last_modified)
+ ```
+- **Set a polite User-Agent** identifying your app and a contact URL.
+- **Limit display length.** Truncate long summaries to keep the terminal readable.
+- **Validate input.** When letting the user open entry N, check N is in range.
+
+## Real-World Applications
+
+- Personal news aggregators (think Feedly, NetNewsWire).
+- Podcast catchers — podcasts publish RSS feeds with `` tags pointing to MP3s.
+- Monitoring tools — turn build-failure feeds, CVE feeds, or pricing feeds into alerts.
+- Content backups — archive a blog by downloading every entry.
+- Cross-posting pipelines — read from one platform's feed, post to another.
## Educational Value
This project teaches:
-- **Web Data Parsing**: Working with RSS/XML feeds and structured web data
-- **HTTP Requests**: Understanding how to fetch content from web APIs
-- **Data Extraction**: Processing and organizing information from external sources
-- **Browser Integration**: Connecting Python applications with web browsers
-- **Feed Standards**: Learning RSS and Atom feed formats and specifications
-- **Content Aggregation**: Building systems that collect and organize information
-- **Real-Time Data**: Working with live, updating content sources
-- **User Experience**: Creating applications that bridge data and user interaction
-
-Perfect for understanding web data processing, content aggregation, and building practical tools for information consumption.
+- **Working with web data** — HTTP requests are hidden but real.
+- **Structured data extraction** — feeds, JSON APIs, scraped pages all share the same patterns.
+- **Defensive coding** — `entry.get(key, default)` is the difference between robust and brittle.
+- **Module ecosystem** — when to reach for a library (`feedparser`) instead of parsing XML by hand.
+- **User experience** — even a CLI tool benefits from clean formatting and gentle error messages.
+
+## Next Steps
+
+- Build a **subscription manager** that stores feed URLs in a JSON file and lets the user add/remove them.
+- Add a **search index** with `sqlite3` so you can grep entries across hundreds of feeds.
+- Turn it into a **web service** with Flask (see [Basic Web Server](./basicwebserver)) that shows your feeds on a web page.
+- Bridge feeds to **Discord or Slack** with their webhook APIs.
+- Plug into a **TTS engine** (`pyttsx3`) for an audio news briefing.
+
+## Conclusion
+
+A few dozen lines of Python plus `feedparser` is enough to replicate the core of a commercial RSS reader. You learned to fetch a feed, walk its entries, handle missing fields gracefully, and open articles in the browser. The same patterns will serve you anywhere you consume structured data from the web. The full source is on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/rssfeedreader.py). Explore more projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/simple-blog-system.mdx b/src/content/docs/projects/Beginners/simple-blog-system.mdx
index 8834f717..836a7c9b 100644
--- a/src/content/docs/projects/Beginners/simple-blog-system.mdx
+++ b/src/content/docs/projects/Beginners/simple-blog-system.mdx
@@ -1,6 +1,6 @@
---
title: Simple Blog System
-description: A comprehensive command-line blog management system with post creation, editing, tagging, and export features built with Python
+description: Build a CLI blog manager in Python with posts, tags, search, draft/publish workflow, JSON persistence, and a path to Markdown, SQLite, and a Flask web frontend.
sidebar:
order: 47
hero:
@@ -13,273 +13,301 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+A blog system in fewer than 300 lines of Python exercises every fundamental of a real content app: data modeling, persistence, CRUD, search, tags, draft vs. published workflow, statistics, and export. In this tutorial you build a CLI blog manager that stores posts in JSON, supports tags and search, tracks publication status, and exports to plain text or Markdown. Then we discuss the natural growth path: SQLite, Markdown rendering, a Flask web UI, and finally a published static-site generator.
-A comprehensive command-line blog management system that allows users to create, edit, manage, and export blog posts with advanced features like tagging, search, and statistics. This project demonstrates content management, data persistence, and search functionality.
+You will learn:
+- Designing a data class for a domain object (`BlogPost`).
+- Persisting a list of objects to JSON safely.
+- Implementing search, tag filtering, and statistics.
+- Building a non-trivial menu-driven CLI without spaghetti.
+- The path from CLI → web app → static site.
-## 🎯 Project Overview
+## Prerequisites
+- Python 3.6 or above (3.7+ recommended for `dataclasses`).
+- A text editor or IDE.
+- Comfort with classes, lists, and dictionaries.
+- Familiarity with the [Personal Diary](./personaldiary) project — same JSON + class pattern.
-This project creates a complete blog management system with:
-- Post creation and editing capabilities
-- Tag management system
-- Search and filtering functionality
-- Publishing workflow (draft/published states)
-- Statistics and analytics
-- Export functionality
-- Persistent data storage using JSON
+## Getting Started
-## ✨ Features
+### Create the project
+1. Create folder `simple-blog`.
+2. Inside, create `simpleblogsystem.py`.
-### Post Management
-- **Create Posts**: Write new blog posts with title, content, and author
-- **Edit Posts**: Update existing post content and metadata
-- **Delete Posts**: Remove posts with confirmation
-- **Publishing Workflow**: Draft and publish post states
+### Write the code
+
-### Organization & Search
-- **Tag System**: Add and remove tags for better organization
-- **Search Posts**: Search by title, content, or author
-- **Filter by Author**: View posts from specific authors
-- **Filter by Tags**: Find posts with specific tags
+### Run it
+```bash title="run"
+python simpleblogsystem.py
+```
-### Analytics & Export
-- **Statistics Dashboard**: View blog metrics and analytics
-- **Export Functionality**: Export posts to text files
-- **Most Common Tags**: Track popular tags
-- **Author Analytics**: Monitor author contributions
+```
+1. Create post
+2. List all
+3. Search
+4. View by ID
+5. Edit
+6. Delete
+7. Publish/unpublish
+8. Manage tags
+9. Statistics
+10. Export
+11. Quit
+> 1
+Title: First post
+Author: Madhur
+Content (end with empty line):
+> Hello world.
+>
+Created post 1700000000.
+```
-### Data Persistence
-- **JSON Storage**: Automatic saving and loading of blog data
-- **Backup Safety**: Error handling for data operations
-- **Unicode Support**: Handle international characters properly
+## Step-by-Step Explanation
-## 🛠️ Technical Implementation
+### 1. The `BlogPost` data class
+```python title="simpleblogsystem.py"
+from dataclasses import dataclass, field, asdict
+from datetime import datetime
+from time import time as now_ts
-### Class Structure
-```python title="simpleblogsystem.py" showLineNumbers{1}
+@dataclass
class BlogPost:
- def __init__(self, title, content, author="Anonymous", post_id=None):
- # Initialize post with metadata
- # Generate unique ID and timestamps
-
- def to_dict(self):
- # Convert post to dictionary for storage
-
- def add_tag(self, tag):
- # Add tags with duplicate prevention
-
+ title: str
+ content: str
+ author: str = "Anonymous"
+ id: str = field(default_factory=lambda: str(int(now_ts())))
+ created_at: str = field(default_factory=lambda: datetime.now().isoformat())
+ updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
+ tags: list[str] = field(default_factory=list)
+ published: bool = False
+
+ def to_dict(self): return asdict(self)
+ @classmethod
+ def from_dict(cls, d): return cls(**d)
+
+ def add_tag(self, tag: str):
+ tag = tag.strip().lower()
+ if tag and tag not in self.tags:
+ self.tags.append(tag)
+ self.touch()
+
+ def remove_tag(self, tag: str):
+ if tag in self.tags:
+ self.tags.remove(tag)
+ self.touch()
+
+ def update(self, title=None, content=None):
+ if title is not None: self.title = title
+ if content is not None: self.content = content
+ self.touch()
+
def publish(self):
- # Change post status to published
-
-class SimpleBlogSystem:
- def __init__(self, data_file="blog_data.json"):
- # Initialize blog system with data persistence
-
- def create_post(self, title, content, author):
- # Create and store new blog post
-
- def search_posts(self, query):
- # Implement comprehensive search functionality
-```
+ self.published = True
+ self.touch()
-### Key Components
-
-#### Data Models
-- **BlogPost Class**: Represents individual blog posts with metadata
-- **Timestamp Tracking**: Creation and modification timestamps
-- **Unique IDs**: Automatic generation of post identifiers
-- **Status Management**: Draft and published states
-
-#### Storage System
-- **JSON Persistence**: Automatic saving and loading
-- **Error Handling**: Robust file operation error handling
-- **Encoding Support**: UTF-8 encoding for international content
-- **Data Integrity**: Validation and backup mechanisms
-
-#### Search & Filter
-- **Multi-field Search**: Search across title, content, and author
-- **Case-insensitive**: Flexible search matching
-- **Tag Filtering**: Find posts by specific tags
-- **Author Filtering**: View posts from specific authors
-
-## 🚀 How to Run
-
-1. **Install Python**: Ensure Python 3.6+ is installed
-2. **Run the Blog System**:
- ```bash
- python simpleblogsystem.py
- ```
-
-3. **Using the System**:
- - Follow the interactive menu prompts
- - Create your first post
- - Explore all features through the numbered menu
-
-## 💡 Usage Examples
-
-### Creating a New Post
-```python title="simpleblogsystem.py" showLineNumbers{1}
-# Create blog system instance
-blog = SimpleBlogSystem()
-
-# Create a new post
-post = blog.create_post(
- title="My First Blog Post",
- content="This is the content of my first post...",
- author="John Doe"
-)
-
-# Add tags
-post.add_tag("python")
-post.add_tag("programming")
-
-# Publish the post
-post.publish()
+ def touch(self):
+ self.updated_at = datetime.now().isoformat()
```
-
-### Searching and Filtering
-```python title="simpleblogsystem.py" showLineNumbers{1}
-# Search posts by content
-results = blog.search_posts("python programming")
-
-# Get posts by specific author
-author_posts = blog.get_posts_by_author("John Doe")
-
-# Get posts with specific tag
-tagged_posts = blog.get_posts_by_tag("python")
-
-# Get only published posts
-published = blog.get_all_posts(published_only=True)
+`@dataclass` gives us a real constructor, repr, and equality without boilerplate. `field(default_factory=...)` defers default computation so each new post gets its own timestamp and ID.
+
+### 2. The `Blog` system class
+```python title="simpleblogsystem.py"
+import json
+from pathlib import Path
+
+class Blog:
+ def __init__(self, datafile="blog_data.json"):
+ self.datafile = Path(datafile)
+ self.posts: list[BlogPost] = []
+ self.load()
+
+ def load(self):
+ if self.datafile.exists() and self.datafile.stat().st_size > 0:
+ data = json.loads(self.datafile.read_text(encoding="utf-8"))
+ self.posts = [BlogPost.from_dict(d) for d in data]
+
+ def save(self):
+ self.datafile.write_text(
+ json.dumps([p.to_dict() for p in self.posts], indent=2),
+ encoding="utf-8")
+
+ def add(self, post: BlogPost):
+ self.posts.append(post); self.save()
+
+ def by_id(self, post_id: str):
+ return next((p for p in self.posts if p.id == post_id), None)
+
+ def delete(self, post_id: str) -> bool:
+ post = self.by_id(post_id)
+ if post:
+ self.posts.remove(post); self.save(); return True
+ return False
```
-
-### Statistics and Analytics
-```python title="simpleblogsystem.py" showLineNumbers{1}
-# Get comprehensive statistics
-stats = blog.get_post_statistics()
-print(f"Total posts: {stats['total_posts']}")
-print(f"Published: {stats['published_posts']}")
-print(f"Most common tags: {stats['most_common_tags']}")
+Three patterns to notice:
+- **Load on init**, save on every mutation — never lose data.
+- **`next((... for ... if ...), None)`** is the idiomatic way to find-or-None.
+- **Mutating methods always call `save()`** so the file matches memory.
+
+### 3. Search and filter
+```python title="simpleblogsystem.py"
+def search(self, query: str):
+ q = query.lower()
+ return [p for p in self.posts
+ if q in p.title.lower()
+ or q in p.content.lower()
+ or q in p.author.lower()]
+
+def by_author(self, author: str):
+ return [p for p in self.posts if p.author == author]
+
+def by_tag(self, tag: str):
+ return [p for p in self.posts if tag.lower() in p.tags]
+
+def published_only(self):
+ return [p for p in self.posts if p.published]
```
-## 🎨 Menu System
-
-### Main Menu Options
-1. **Create New Post** - Write and save a new blog post
-2. **View All Posts** - Display all posts (including drafts)
-3. **View Published Posts** - Show only published content
-4. **Search Posts** - Find posts by keywords
-5. **View Post by ID** - Display specific post
-6. **Edit Post** - Modify existing post content
-7. **Delete Post** - Remove posts with confirmation
-8. **Manage Tags** - Add or remove post tags
-9. **Publish/Unpublish** - Change post publication status
-10. **View Statistics** - Display blog analytics
-11. **Export Posts** - Save posts to external file
-
-### Interactive Features
-- **Multi-line Content**: Support for long-form content entry
-- **Confirmation Prompts**: Safety checks for destructive operations
-- **Input Validation**: Error handling for user input
-- **Status Indicators**: Clear feedback for all operations
-
-## 🔧 Advanced Features
-
-### Export System
-```python title="simpleblogsystem.py" showLineNumbers{1}
-# Export all published posts
-blog.export_posts("my_blog_export.txt", published_only=True)
-
-# Export all posts including drafts
-blog.export_posts("complete_backup.txt", published_only=False)
+### 4. Statistics
+```python title="simpleblogsystem.py"
+from collections import Counter
+
+def stats(self):
+ tag_counts = Counter(t for p in self.posts for t in p.tags)
+ return {
+ "total": len(self.posts),
+ "published": sum(1 for p in self.posts if p.published),
+ "drafts": sum(1 for p in self.posts if not p.published),
+ "authors": len({p.author for p in self.posts}),
+ "top_tags": tag_counts.most_common(5),
+ }
```
-### Tag Management
-```python title="simpleblogsystem.py" showLineNumbers{1}
-# Add multiple tags
-post.add_tag("python")
-post.add_tag("tutorial")
-post.add_tag("beginner")
-
-# Remove tags
-post.remove_tag("beginner")
-
-# Get posts by tag
-python_posts = blog.get_posts_by_tag("python")
+### 5. Export to text or Markdown
+```python title="simpleblogsystem.py"
+def export(self, filename: str, published_only=True):
+ posts = self.published_only() if published_only else self.posts
+ lines = []
+ for p in posts:
+ lines.append(f"# {p.title}")
+ lines.append(f"_by {p.author} on {p.created_at[:10]}_")
+ if p.tags: lines.append("Tags: " + ", ".join(f"#{t}" for t in p.tags))
+ lines.append(""); lines.append(p.content); lines.append("\n---\n")
+ Path(filename).write_text("\n".join(lines), encoding="utf-8")
```
-
-### Content Updates
-```python title="simpleblogsystem.py" showLineNumbers{1}
-# Update post content
-post.update_content(
- title="Updated Title",
- content="New content here..."
-)
-
-# Automatic timestamp updating
-print(post.updated_at) # Shows latest modification time
+The same output is readable as plain text *and* renders nicely on GitHub or any Markdown viewer.
+
+## Multi-line Content
+
+```python title="multiline.py"
+def read_multiline(prompt: str) -> str:
+ print(prompt + " (end with an empty line)")
+ lines = []
+ while True:
+ line = input("> ")
+ if line == "": break
+ lines.append(line)
+ return "\n".join(lines)
```
-
-## 📊 Data Structure
-
-### Post Format
-```json
-{
- "id": "1693123456",
- "title": "Sample Blog Post",
- "content": "This is the post content...",
- "author": "John Doe",
- "created_at": "2024-01-01T12:00:00.000000",
- "updated_at": "2024-01-01T12:00:00.000000",
- "tags": ["python", "programming"],
- "published": true
-}
+Used in the create-post and edit-post flows so users can write paragraphs.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `json.decoder.JSONDecodeError` on first run | Empty/missing file | Check `stat().st_size > 0` |
+| Duplicate IDs | Reused timestamp | Append a UUID or check uniqueness |
+| Loaded post has wrong field shape | Schema changed | Use `.get()` in `from_dict` or migrate explicitly |
+| Tags case-insensitive matching fails | Mixed case in store | Normalize on `add_tag` (`tag.lower()`) |
+| Lost edits | Forgot `save()` | Save at the end of every mutating method |
+| Slow with 10k posts | Linear scans | Migrate to SQLite with indexes |
+
+## Variations to Try
+
+### 1. Markdown rendering
+`pip install markdown` and convert post content to HTML when exporting:
+```python title="md.py"
+import markdown
+html = markdown.markdown(p.content)
```
-### Statistics Output
-```python title="simpleblogsystem.py" showLineNumbers{1}
-{
- 'total_posts': 10,
- 'published_posts': 8,
- 'draft_posts': 2,
- 'total_authors': 3,
- 'total_tags': 15,
- 'most_common_tags': [('python', 5), ('tutorial', 3)]
-}
+### 2. Slug-based URLs
+Generate a URL-safe slug from the title (e.g. `My First Post` → `my-first-post`):
+```python title="slug.py"
+import re
+def slugify(s): return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
```
-## 🛡️ Error Handling
+### 3. Static site export
+Iterate posts → render each to an HTML file via a template (Jinja2). The result is a deployable site. Hosts: GitHub Pages, Netlify, Cloudflare Pages — all free.
+
+### 4. RSS feed
+Generate `feed.xml` with the standard RSS 2.0 schema. See [RSS Feed Reader](./rssfeedreader) for the reader side.
-- **File Operations**: Graceful handling of file read/write errors
-- **JSON Parsing**: Recovery from corrupted data files
-- **User Input**: Validation and sanitization of all inputs
-- **ID Management**: Duplicate prevention and unique identification
+### 5. Web frontend with Flask
+See [Basic Web Server](./basicwebserver). Two routes: `GET /` for the index, `GET /post/` for individual posts.
-## 📚 Learning Objectives
+### 6. Admin authentication
+Protect create/edit/delete with a password. `werkzeug.security.generate_password_hash` is the right tool.
-- **Object-Oriented Programming**: Class design and inheritance
-- **File I/O Operations**: JSON data persistence
-- **Data Structures**: Lists, dictionaries, and sets
-- **Error Handling**: Exception management and user feedback
-- **CLI Development**: Interactive command-line interfaces
+### 7. SQLite backend
+JSON works through ~thousands of posts. Past that, swap to SQLite with FTS5 for full-text search:
+```sql title="schema.sql"
+CREATE VIRTUAL TABLE posts USING fts5(title, content, author, tags);
+SELECT * FROM posts WHERE posts MATCH 'python';
+```
+
+### 8. Comments
+Sub-collection per post. Moderation queue, spam filter (`pip install python-akismet`).
+
+### 9. Image upload
+A simple upload endpoint (Flask `request.files`) that saves to `static/uploads/` and returns a Markdown image reference.
-## 🎯 Potential Enhancements
+### 10. Multi-author
+Author becomes its own entity with email and avatar. Add an `Author` dataclass and join on `author_id`.
-1. **Web Interface**: Add Flask web interface
-2. **Markdown Support**: Rich text formatting
-3. **User Authentication**: Multi-user blog system
-4. **Database Integration**: SQLite or PostgreSQL storage
-5. **RSS Feed**: Generate RSS feeds for published posts
-6. **Comment System**: Add reader comments functionality
-7. **Image Support**: Embed images in posts
-8. **Template System**: Customizable post templates
+### 11. Drafts and scheduling
+Add a `publish_at: datetime | None` field. Background job promotes posts when their time arrives.
-## 🏆 Project Completion
+### 12. Templates
+Pre-fill new posts from templates (`how-to-tutorial`, `code-review`, `book-notes`).
-This Simple Blog System demonstrates:
-- ✅ Complete CRUD (Create, Read, Update, Delete) operations
-- ✅ Data persistence and management
-- ✅ Advanced search and filtering
-- ✅ Interactive command-line interface
-- ✅ Comprehensive error handling
-- ✅ Professional code organization
+## Architecture Path
-Perfect for beginners learning data management and intermediate developers exploring content management systems!
+```
+CLI (JSON) ← you are here
+ ↓
+CLI (SQLite)
+ ↓
+Flask web (SQLite)
+ ↓
+Flask + JWT auth + API
+ ↓
+Static generator (Hugo / Astro-style) ← optional fork
+```
+Take one step at a time. Trying to jump from a JSON CLI to a multi-tenant Markdown CMS in one go is the usual cause of abandoned blog projects.
+
+## Real-World Applications
+- **Personal knowledge base** — Zettelkasten / digital garden.
+- **Internal wiki** for a team — keep notes versioned, search them.
+- **CMS prototypes** before committing to WordPress/Ghost.
+- **Documentation backends** for small products.
+- **Project journals** — track decisions and rationale per project.
+
+## Educational Value
+- **Domain modeling** — what fields belong on `BlogPost`?
+- **Persistence patterns** — JSON ↔ class via `to_dict` / `from_dict`.
+- **CRUD UX** — confirmations, validation, friendly errors.
+- **Search and filter** — list comprehensions as a query language.
+- **Statistics with `Counter`** — the small standard-library jewel.
+
+## Next Steps
+- Add **Markdown rendering** to the export.
+- Add **slug-based filenames** for static export.
+- Migrate to **SQLite** with FTS5 full-text search.
+- Wrap with **Flask** for a real blog at `localhost:5000`.
+- Add **RSS feed** generation and cross-link with [RSS Feed Reader](./rssfeedreader).
+- Pair with [Personal Diary](./personaldiary) — same architecture, different domain.
+
+## Conclusion
+You built a complete blog manager with CRUD, search, tags, statistics, and export — all backed by a single JSON file you can read in a text editor. Every CMS in the world reuses these primitives; the difference is scale, not concept. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/simpleblogsystem.py). Find more content-management projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/simplecalculator.mdx b/src/content/docs/projects/Beginners/simplecalculator.mdx
index 20cc89f8..be84cfec 100644
--- a/src/content/docs/projects/Beginners/simplecalculator.mdx
+++ b/src/content/docs/projects/Beginners/simplecalculator.mdx
@@ -1,6 +1,6 @@
---
title: Simple Calculator
-description: Create a simple calculator in Python. This is a great project for beginners. You will learn how to use functions, return values, and if statements.
+description: Build a fully functional command-line calculator in Python. Learn functions, return values, conditionals, the math module, input validation, and how to grow a basic script into a robust user tool.
sidebar:
order: 2
hero:
@@ -14,25 +14,45 @@ import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Creating a simple calculator is a great project for beginners. You will learn how to use functions, return values, and if statements. In this project, you will create a calculator that can add, subtract, multiply, and divide two numbers. You can also calculate the square root of a number and power of a number. This application is a command-line application. You can run this application in the terminal.
+A calculator is the perfect second project. It is small enough to finish in one sitting and large enough to teach you the four pillars of procedural Python: **functions**, **control flow**, **user input**, and **error handling**. In this tutorial you will build a command-line calculator that performs the four basic operations plus square root and exponentiation, runs in a loop until the user chooses to exit, and handles invalid input gracefully.
+
+By the end you will have practiced:
+- Defining and calling functions with parameters and return values.
+- Using the `math` module from Python's standard library.
+- Branching with `if`/`elif`/`else`.
+- Looping with `while True` plus `break`.
+- Converting strings from `input()` into numbers using `float()`.
+- Catching errors with `try`/`except`.
## Prerequisites
-- Python 3.6 or above
-- Text Editor or IDE
+- Python 3.6 or above.
+- A text editor or IDE (VS Code recommended).
+- Familiarity with running a Python file from the terminal (see [Hello World](./helloworld)).
+
+## Concepts You Will Use
+
+| Concept | What it is |
+|---|---|
+| **Function** | A reusable named block of code. Defined with `def`. |
+| **Parameter** | A variable a function expects when called. |
+| **Return value** | The result a function hands back with `return`. |
+| **`math` module** | Standard-library module providing `sqrt`, `pow`, `pi`, `log`, etc. |
+| **Type conversion** | Turning a string like `"3.14"` into a float with `float()`. |
+| **Exception** | An error object Python raises when something goes wrong, such as dividing by zero. |
## Getting Started
-### Create a new project
-1. Create a new project folder and name it `simple-calculator`.
-2. Create a new file and name it `calculator.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Open the `calculator.py` file in your text editor or IDE.
+
+### Create the project
+1. Make a folder named `simple-calculator`.
+2. Inside it, create a file named `calculator.py`.
+3. Open the folder in your code editor.
### Write the code
-1. Add following code to the `calculator.py` file.
+Paste the following into `calculator.py`:
-2. Save the file.
-3. Open the terminal and navigate to the project folder.
-4. Run the following command to run the application.
+
+Save the file. Open the terminal, `cd` to the folder, and run:
+
```cmd title="command" showLineNumbers{1}
C:\Users\username\Documents\simple-calculator> python calculator.py
Select Operation.
@@ -53,31 +73,173 @@ Are you sure you want to exit? (Y/N): Y
Exiting...
```
-## Explanation
-In this application, we create some functions to perform the mathematical operations. We use the `math` module to calculate the square root and power of a number.
+## Step-by-Step Explanation
+
+### 1. Import the math module
+```python title="calculator.py"
+import math
+```
+The `math` module gives you mathematical helpers you would otherwise have to write yourself. We use `math.sqrt(x)` for square roots and `math.pow(x, y)` for exponentiation. (Python also has the `**` operator for powers, but using `math.pow` keeps the code consistent and explicit.)
+
+### 2. Define one function per operation
+```python title="calculator.py"
+def add(x, y):
+ return x + y
+
+def subtract(x, y):
+ return x - y
+
+def multiply(x, y):
+ return x * y
+
+def divide(x, y):
+ return x / y
+```
+Each function takes two numbers, performs one operation, and returns the result. This is **separation of concerns** — every piece does exactly one thing. It is easier to test, easier to read, and easier to extend.
+
+### 3. The main loop
+```python title="calculator.py"
+while True:
+ print("Select Operation.")
+ print("1. Addition")
+ # …
+ choice = input("Enter Choice (1/2/3/4/5/6/E/R): ")
+```
+`while True` keeps the menu visible until the user explicitly exits. Without the loop, the program would run once and quit — frustrating for anyone doing several calculations in a row.
-We use the `while` loop to run the application until the user exits the application. We use the `if` statement to check the user input and perform the operation accordingly. We use the `input()` function to get the user input. We use the `float()` function to convert the user input to a floating-point number. We use the `print()` function to print the output to the terminal. We use the `break` statement to exit the `while` loop. We use the `upper()` function to convert the user input to uppercase. We use the `elif` statement to check multiple conditions. We use the `else` statement to execute the code if the `if` statement condition is `False`. We use the `return` statement to return the value from the function. We use the `def` keyword to define a function. We use the `import` keyword to import the `math` module. We use the `math.sqrt()` function to calculate the square root of a number. We use the `math.pow()` function to calculate the power of a number.
+### 4. Dispatching the choice
+```python title="calculator.py"
+if choice == "1":
+ num1 = float(input("Enter First Number: "))
+ num2 = float(input("Enter Second Number: "))
+ print(num1, "+", num2, "=", add(num1, num2))
+elif choice == "2":
+ # …
+```
+- `input()` always returns a string. `float()` converts it to a floating-point number so arithmetic works.
+- The chain of `if`/`elif`/`else` picks the right function based on the menu choice.
+- An `else` at the bottom handles unknown inputs.
-#### The Usage Guide
-- If you want to perform the addition operation, you can press the `1` key.
-- If you want to perform the subtraction operation, you can press the `2` key.
-- If you want to perform the multiplication operation, you can press the `3` key.
-- If you want to perform the division operation, you can press the `4` key.
-- If you want to calculate the square root of a number, you can press the `5` key.
-- If you want to calculate the power of a number, you can press the `6` key.
-- If you want to exit the application, you can press the `E` or `e` key.
-- If you want to restart the application, you can press the `R` or `r` key.
+### 5. Exit confirmation
+```python title="calculator.py"
+elif choice.upper() == "E":
+ confirm = input("Are you sure you want to exit? (Y/N): ")
+ if confirm.upper() == "Y":
+ print("Exiting...")
+ break
+```
+Using `.upper()` makes the check case-insensitive — `e`, `E`, `y`, and `Y` all work.
+
+## Add Robustness: Error Handling
+
+The version above crashes if a user types `abc` when asked for a number. Add a `try`/`except` block to handle it:
+
+```python title="safer_input.py"
+def get_number(prompt):
+ while True:
+ try:
+ return float(input(prompt))
+ except ValueError:
+ print("Please enter a valid number.")
+```
+Replace each `float(input(...))` call with `get_number("...")`. Now a typo no longer crashes the program — it asks again.
+
+Likewise, division by zero should be friendly, not fatal:
+```python title="safe_divide.py"
+def divide(x, y):
+ if y == 0:
+ return "undefined (cannot divide by zero)"
+ return x / y
+```
+
+## Common Mistakes
+
+| Problem | What happened | Fix |
+|---|---|---|
+| `TypeError: can only concatenate str` | You tried `"=" + add(...)` instead of using `,` in `print` | Use commas in `print` or an f-string |
+| `ValueError: could not convert string to float` | User typed letters | Wrap `float(input(...))` in `try`/`except` |
+| Loop never exits | `break` is indented wrong, or condition is always false | Confirm `break` is inside the `if`, not after it |
+| Decimal results everywhere | Used `float` even for whole numbers | Display with `int(result)` when you know it is whole, or format with `f"{result:.2f}"` |
+
+## Variations to Try
+
+### 1. More operations
+```python title="more_ops.py"
+def modulo(x, y): return x % y
+def floor_div(x, y): return x // y
+def factorial(n): return math.factorial(int(n))
+def log(x): return math.log(x)
+```
+
+### 2. Refactor with a dispatch dictionary
+A growing `if`/`elif` chain becomes unwieldy. Replace it with a dictionary that maps choices to functions:
+
+```python title="dispatch.py"
+OPERATIONS = {
+ "1": ("Addition", add),
+ "2": ("Subtraction", subtract),
+ "3": ("Multiplication", multiply),
+ "4": ("Division", divide),
+}
+
+choice = input("Choice: ")
+if choice in OPERATIONS:
+ name, func = OPERATIONS[choice]
+ a, b = get_number("a: "), get_number("b: ")
+ print(f"{name}: {func(a, b)}")
+```
+Adding a new operation is now one line.
+
+### 3. Expression evaluator
+Instead of menu-driven input, parse a typed expression like `2 + 3 * 4`:
+```python title="expr.py"
+import ast, operator as op
+
+OPS = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv}
+
+def evaluate(node):
+ if isinstance(node, ast.Constant):
+ return node.value
+ if isinstance(node, ast.BinOp):
+ return OPS[type(node.op)](evaluate(node.left), evaluate(node.right))
+ raise ValueError("unsupported")
+
+tree = ast.parse("2 + 3 * 4", mode="eval")
+print(evaluate(tree.body)) # 14
+```
+This safely evaluates math expressions without using the dangerous built-in `eval()`.
+
+### 4. GUI calculator
+Use Tkinter to build buttons in a grid. See the [Calculator GUI](./calculatorgui) project on Python Central Hub for a full walkthrough.
+
+### 5. Calculation history
+Keep a list of past results and let the user reuse the previous answer with `ans`:
+```python title="history.py"
+history = []
+# after each successful calc:
+history.append(result)
+# allow input "ans" to mean history[-1]
+```
+
+## Best Practices Demonstrated
+- **One function, one job.** Each operation is its own function.
+- **Use the standard library.** `math` is already there — do not roll your own square root.
+- **Validate at the boundary.** Convert and check input the moment it enters the program.
+- **Handle errors close to where they happen.** Division by zero is a `divide()` problem, not a *whole program* problem.
+
+## Real-World Applications
+- Interactive shells for scientific work (think mini-REPL).
+- Form back-ends that compute totals from user input.
+- Bot commands (`!calc 2+2`) in Discord/Slack integrations.
+- Foundations for spreadsheet engines and expression evaluators.
## Next Steps
-You can add more mathematical operations to this application. You can add the following operations to this application.
-- Factorial
-- Logarithm
-- Trigonometric Functions
-- Exponential Functions
-- Hyperbolic Functions
-- Rounding Functions
-- Summation Functions
-- Product Functions
-- More Mathematical Functions
-
-Congratulations 🎉 you have successfully created a simple calculator in Python. You can find the complete source code of this project on [Github](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/calculator.py)
\ No newline at end of file
+Extend the calculator with any of these:
+- **Trigonometry**: `math.sin`, `math.cos`, `math.tan` (remember to convert degrees with `math.radians`).
+- **Memory keys**: `M+`, `M-`, `MR`, `MC` like a real desktop calculator.
+- **Calculation history file**: persist results to `history.txt` so they survive restarts.
+- **Unit conversion mode**: kilometers ↔ miles, kilograms ↔ pounds, etc.
+- **Graphing mode**: pair with `matplotlib` to plot a function the user types.
+
+## Conclusion
+A calculator looks simple, but it is one of the most honest beginner projects: every feature you add forces you to write better code. You used Python's built-in `math` module, separated each operation into its own function, looped a menu, and converted user input safely. Find more progressive beginner projects on Python Central Hub. The full source is on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/calculator.py).
diff --git a/src/content/docs/projects/Beginners/simplereminderapp.mdx b/src/content/docs/projects/Beginners/simplereminderapp.mdx
index 2e2c2336..3cc6cb78 100644
--- a/src/content/docs/projects/Beginners/simplereminderapp.mdx
+++ b/src/content/docs/projects/Beginners/simplereminderapp.mdx
@@ -1,6 +1,6 @@
---
title: Simple Reminder App
-description: A Python reminder application that sends desktop notifications after a specified time. Perfect for setting quick reminders and staying organized.
+description: Build a Python reminder tool with desktop notifications. Learn time delays, cross-platform alerts, scheduling, persistent multi-reminder support, and how to grow it into a tray-based assistant.
sidebar:
order: 29
hero:
@@ -13,162 +13,275 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Simple Reminder App is a practical Python application that helps users set reminders with desktop notifications. The app allows users to input a reminder message and specify when they want to be reminded (in minutes). After the specified time, the application displays a desktop notification with the reminder message. This project demonstrates working with time delays, desktop notifications, and user input handling. The app includes versions for both Windows (using plyer) and Mac (using pync).
+A reminder app is a tiny program with huge personal-productivity value: type a message and a delay, and the computer pings you when the time arrives. In this tutorial you will build the single-shot version with `plyer` for desktop notifications, then evolve it into a multi-reminder scheduler that survives restarts, supports absolute times (not just "in N minutes"), runs in the background, and works on Windows, macOS, and Linux.
-## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
-- plyer module (for Windows)
-- pync module (for Mac)
+You will learn:
+- How `time.sleep` actually works (and when it is the wrong tool).
+- How **`plyer`** abstracts notification systems across operating systems.
+- The trade-offs of `time.sleep` vs. **scheduled-job** libraries.
+- How to persist reminders in JSON so they survive restarts.
+- How to background the process so a closed terminal does not kill your reminders.
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download). You must have the required modules installed on your computer. Install the appropriate module based on your operating system:
+## Prerequisites
+- Python 3.6 or above.
+- A code editor or IDE.
+- Notifications enabled for Python on your OS.
-For Windows:
-```cmd title="command" showLineNumbers{1} {1}
-C:\Users\Your Name>pip install plyer
+## Install Dependencies
+```bash title="install"
+pip install plyer
```
-For Mac:
-```cmd title="command" showLineNumbers{1} {1}
-$ pip install pync
+For native macOS notifications with sound, install **`pync`** instead:
+```bash title="mac"
+pip install pync
```
## Getting Started
-#### Create a Project
-1. Create a folder named `simple-reminder-app`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `simplereminderapp.py`.
-4. Copy the given code and paste it in your `simplereminderapp.py` file.
-
-## Write the Code
-1. Copy and paste the following code in your `simplereminderapp.py` file.
+
+### Create the project
+1. Create folder `simple-reminder-app`.
+2. Inside, create `simplereminderapp.py`.
+
+### Write the code
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `simple-reminder-app`.
-```cmd title="command" showLineNumbers{1} {1-20}
+
+### Run it
+```cmd title="command"
C:\Users\Your Name\simple-reminder-app> python simplereminderapp.py
Welcome to the Reminder App!
-This app will remind you about anything you want!
-Let's get started!
-What would you like to be reminded about?
-Enter your reminder: Call mom
-In how many minutes would you like to be reminded?
-Enter minutes: 2
+What would you like to be reminded about? Call mom
+In how many minutes? 2
Reminder set!
-# ... wait for 2 minutes ...
-# Desktop notification appears: "Reminder: Call mom"
-Reminder: Call mom
+# … 2 minutes pass …
+# Desktop notification pops up: "Reminder: Call mom"
Reminder Completed!
-Would you like to set another reminder?
-Enter yes or no: no
-Thank you for using the Reminder App!
+Would you like to set another reminder? (y/n) n
+Thanks!
```
-## Explanation
-1. Import the required modules.
-```python title="simplereminderapp.py" showLineNumbers{1} {1-4}
+## Step-by-Step Explanation
+
+### 1. Imports
+```python title="simplereminderapp.py"
import time
-import datetime
from plyer import notification
```
+- `time.sleep(seconds)` pauses execution.
+- `notification.notify(...)` pops a desktop alert.
-2. Create a utility function for printing with pauses.
-```python title="simplereminderapp.py" showLineNumbers{1} {1-4}
-def print_pause(message_to_print):
- print(message_to_print)
- time.sleep(2)
+### 2. The pacing helper
+```python title="simplereminderapp.py"
+def print_pause(message, seconds=1):
+ print(message)
+ time.sleep(seconds)
```
+Small pauses between prompts make the CLI feel less robotic.
-3. Create the main reminder function.
-```python title="simplereminderapp.py" showLineNumbers{1} {1-15}
+### 3. Collect the reminder
+```python title="simplereminderapp.py"
def reminder():
- print_pause("What would you like to be reminded about?")
- reminder = input("Enter your reminder: ")
- print_pause("In how many minutes would you like to be reminded?")
- minutes = input("Enter minutes: ")
+ msg = input("What would you like to be reminded about? ")
+ while True:
+ raw = input("In how many minutes? ")
+ try:
+ minutes = float(raw)
+ if minutes <= 0:
+ raise ValueError("must be positive")
+ break
+ except ValueError:
+ print("Please enter a positive number.")
print_pause("Reminder set!")
- time.sleep(int(minutes) * 60)
- notification.notify(
- title = "Reminder",
- message = reminder,
- timeout = 10
- )
- print_pause("Reminder: " + reminder)
- print_pause("Reminder Completed!")
- play_again()
+ time.sleep(minutes * 60)
+ notification.notify(title="Reminder", message=msg, timeout=10)
```
+- The `while True` loop is the canonical **input validation** pattern.
+- `time.sleep(minutes * 60)` blocks until the time arrives. Simple, but the script is unresponsive while it sleeps — a single typo in the message is unfixable.
-4. Create a function to handle multiple reminders.
-```python title="simplereminderapp.py" showLineNumbers{1} {1-15}
+### 4. Replay
+```python title="simplereminderapp.py"
def play_again():
- print_pause("Would you like to set another reminder?")
while True:
- choice = input("Enter yes or no: ")
- if choice == "yes":
- print_pause("Great!")
- reminder()
- break
- elif choice == "no":
- print_pause("Thank you for using the Reminder App!")
- break
- else:
- print_pause("Please enter yes or no: ")
+ ans = input("Set another reminder? (y/n) ").strip().lower()
+ if ans in ("y", "yes"):
+ return reminder()
+ if ans in ("n", "no"):
+ print("Thanks!")
+ return
+ print("Please answer y or n.")
```
-5. Create an introduction function and start the app.
-```python title="simplereminderapp.py" showLineNumbers{1} {1-8}
-def intro():
- print_pause("Welcome to the Reminder App!")
- print_pause("This app will remind you about anything you want!")
- print_pause("Let's get started!")
- reminder()
+## The `time.sleep` Problem
+
+The simple version blocks until the timer fires. While `sleep(120)` runs:
+- You cannot add a second reminder.
+- You cannot cancel it.
+- If the laptop sleeps, the timer pauses too — so a 30-minute reminder might fire 90 minutes late.
+
+For a single quick reminder, this is fine. For anything serious, use a scheduler.
-intro()
+## Multi-Reminder Version
+
+Use **threads** so each reminder runs independently:
+
+```python title="multi.py"
+import threading, time
+from plyer import notification
+
+def fire_later(message: str, seconds: float):
+ time.sleep(seconds)
+ notification.notify(title="Reminder", message=message, timeout=10)
+
+def add_reminder(message: str, minutes: float):
+ t = threading.Thread(target=fire_later, args=(message, minutes * 60), daemon=True)
+ t.start()
+ print(f"Scheduled '{message}' in {minutes} minutes.")
+
+while True:
+ cmd = input("> ").strip()
+ if cmd == "quit":
+ break
+ if cmd:
+ msg, mins = cmd.rsplit(" ", 1)
+ add_reminder(msg, float(mins))
```
+Now you can keep adding reminders while previous ones wait. `daemon=True` lets the program exit even if reminders are still pending.
+
+## Absolute-Time Reminders
+
+"At 14:30" is often more useful than "in 90 minutes":
+```python title="at_time.py"
+from datetime import datetime, timedelta
+def schedule_at(hhmm: str, message: str):
+ h, m = map(int, hhmm.split(":"))
+ now = datetime.now()
+ target = now.replace(hour=h, minute=m, second=0, microsecond=0)
+ if target <= now:
+ target += timedelta(days=1)
+ seconds = (target - now).total_seconds()
+ add_reminder(message, seconds / 60)
+```
+
+## Persistent Reminders
+
+The thread approach forgets everything on restart. Persist to JSON:
+```python title="persist.py"
+import json, pathlib, time
+STORE = pathlib.Path("reminders.json")
+
+def load():
+ return json.loads(STORE.read_text()) if STORE.exists() else []
+
+def save(reminders):
+ STORE.write_text(json.dumps(reminders))
+
+# at startup, re-schedule each not-yet-fired reminder:
+for r in load():
+ delay = r["due_ts"] - time.time()
+ if delay > 0:
+ add_reminder(r["message"], delay / 60)
+```
+Store entries as `{"message": "...", "due_ts": }` so absolute time is preserved.
+
+## Cross-Platform Notifications
+
+| OS | What works | Note |
+|---|---|---|
+| Windows 10/11 | `plyer.notification` | Uses Windows Toast notifications. |
+| macOS | `pync` (with sound) | `plyer` works but no sound by default. |
+| Linux | `plyer` (uses `notify-send`) | Requires `notify-send` installed (`sudo apt install libnotify-bin`). |
+
+A graceful fallback to terminal output:
+```python title="fallback.py"
+try:
+ notification.notify(title="Reminder", message=msg, timeout=10)
+except Exception:
+ print(f"\a*** REMINDER: {msg} ***\a") # \a rings the terminal bell
+```
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| Notification never appears | OS notifications muted, or Focus Assist on | Check OS notification settings |
+| `ModuleNotFoundError: plyer` | Not installed in the active interpreter | `pip install plyer` |
+| Timer way off after laptop wake | `time.sleep` pauses during sleep | Use `datetime` comparisons in a loop, or use `schedule` |
+| `ValueError: could not convert string to float` | User typed "five" | Validate input in a loop |
+| Process killed when terminal closes | Foreground script | Run with `nohup`, `pythonw.exe`, or as a service |
+
+## Variations to Try
+
+### 1. Recurring reminders
+```python title="recur.py"
+import schedule, time
+schedule.every().day.at("09:00").do(fire, "Take vitamins")
+while True:
+ schedule.run_pending(); time.sleep(30)
+```
+
+### 2. Snooze button
+On fire, show a notification with a 5-minute snooze option. `plyer` itself does not support actions; use `win10toast-click` (Windows) or `pync` (macOS).
+
+### 3. GUI with Tkinter
+A small window with a Message field, an Entry for minutes, and a "Schedule" button. See [Basic Music Player](./basicmusicplayer) for the GUI pattern.
+
+### 4. System-tray app
+Use `pystray` to put a tray icon with "Add reminder" and "Quit" menu entries — no console window needed.
+
+### 5. Voice reminder
+Use **`pyttsx3`** so the reminder is *spoken* aloud:
+```python title="speak.py"
+import pyttsx3
+engine = pyttsx3.init(); engine.say(msg); engine.runAndWait()
+```
+
+### 6. Cloud sync
+Save reminders to a Firebase/Firestore document so they sync across devices.
+
+### 7. Smart parsing
+`pip install dateparser` lets you type *"tomorrow at 9am"* or *"in 2 hours"*:
+```python title="natural.py"
+import dateparser
+target = dateparser.parse("tomorrow at 9am")
+```
+
+### 8. Telegram / Discord bot
+A bot that accepts `/remind 30 buy bread` from your phone and pings you back when due.
-## Platform Support
-This app includes support for both Windows and Mac:
+### 9. Calendar integration
+Pull events from Google Calendar via API; surface them as desktop reminders 5 minutes before each event.
-**Windows Version:** Uses the `plyer` library for cross-platform notifications.
+### 10. Pomodoro mode
+A "/pomodoro" command that fires "Time to break!" every 25 minutes and "Back to work!" every 5.
-**Mac Version:** Uses the `pync` library for native Mac notifications with sound support.
+## Running as a Background Service
-The code includes both implementations - simply uncomment the appropriate version for your operating system.
+So your laptop's terminal is not always open:
+- **Linux:** create a systemd user service that runs the script on boot.
+- **macOS:** add a `launchd` plist with `RunAtLoad`.
+- **Windows:** save as `.pyw` and add a shortcut to `shell:startup`, or use **NSSM** to register as a service.
-## Features
-- Set custom reminder messages
-- Specify reminder time in minutes
-- Desktop notifications with title and message
-- Option to set multiple reminders
-- Cross-platform support (Windows and Mac)
-- User-friendly command-line interface
-- Sound notifications (Mac version)
-- 10-second notification timeout
+## Real-World Applications
+- Medication / hydration reminders.
+- Meeting and standup pings.
+- Pomodoro timer for focused work.
+- Calendar-driven alerts beyond what your OS notification center handles.
+- "Brew tea in 4 minutes" — the original killer app.
-## How to Use
-1. Run the application
-2. Enter your reminder message when prompted
-3. Specify the number of minutes until the reminder
-4. Wait for the specified time
-5. Receive a desktop notification with your reminder
-6. Choose to set another reminder or exit
+## Educational Value
+- **Time handling** — `time.sleep`, `datetime`, scheduling libraries.
+- **Concurrency basics** — threads for parallel timers.
+- **Cross-platform UX** — when an API silently falls short on one OS.
+- **Persistence** — JSON-backed state so the app survives restarts.
+- **Operational thinking** — what does it mean to keep running after you close the terminal?
## Next Steps
-You can enhance this project by:
-- Adding a GUI interface using Tkinter
-- Implementing recurring reminders (daily, weekly)
-- Adding sound customization options
-- Creating a web-based version
-- Adding reminder categories and priorities
-- Implementing snooze functionality
-- Adding reminder history and logs
-- Creating mobile app integration
-
-## Troubleshooting
-- **Notifications not appearing:** Make sure notification permissions are enabled for Python on your system
-- **Module import errors:** Ensure you've installed the correct module for your operating system
-- **Sound not working:** Check system volume and notification settings
+- Implement **multi-reminder threads** above.
+- Add **absolute-time** parsing and **persistent storage**.
+- Wrap with a **Tkinter GUI** or a **system-tray icon**.
+- Use **`dateparser`** for natural-language times.
+- Connect to **Google Calendar** for event-driven reminders.
+- See [Basic Alarm Clock](./basicalarmclock) for the polling-loop variant.
## Conclusion
-In this project, we learned how to create a Simple Reminder App using Python. We explored working with time delays using the `time` module, desktop notifications using platform-specific libraries, and user input handling. This project demonstrates practical application development and cross-platform compatibility considerations. To find more projects like this, you can visit Python Central Hub.
+You built a real reminder tool, learned why `time.sleep` is not the right scheduler, and have a path to a polished tray app. Notification systems, schedulers, and tiny daemons underlie a lot of practical Python work. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/simplereminderapp.py). Explore more productivity projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/simplestopwatch.mdx b/src/content/docs/projects/Beginners/simplestopwatch.mdx
index c4bd526c..9efe2fa7 100644
--- a/src/content/docs/projects/Beginners/simplestopwatch.mdx
+++ b/src/content/docs/projects/Beginners/simplestopwatch.mdx
@@ -1,6 +1,6 @@
---
title: Simple Stopwatch
-description: A Python GUI stopwatch application built with Tkinter that allows users to start, stop, and reset time counting with a clean digital display interface.
+description: Build a Tkinter stopwatch in Python. Learn the event loop, after() scheduling, why time.time() beats counter variables, lap times, HH:MM:SS formatting, keyboard shortcuts, and a CountdownTimer mode.
sidebar:
order: 31
hero:
@@ -13,255 +13,283 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Simple Stopwatch is a Python GUI application that provides basic stopwatch functionality using Tkinter. The application features a large digital display showing elapsed seconds, along with Start, Stop, and Reset buttons for time control. This project demonstrates GUI development, time handling, event-driven programming, and state management in Python. It's an excellent introduction to creating interactive desktop applications with real-time updates.
+A stopwatch is a deceptively small project — under 50 lines and you have a working app. Behind those lines sit two important Tkinter concepts: the **event loop** and **scheduled callbacks via `after`**. We will start with the naïve counter-variable version (which drifts), fix it with `time.time()` for true elapsed-time accuracy, then add HH:MM:SS formatting, lap times, keyboard shortcuts, persistence between sessions, and a countdown mode.
+
+You will leave understanding:
+- Why a recurring `after(1000, fn)` is not a real 1-second timer.
+- The right way to measure elapsed time (record a start timestamp; subtract from `time.time()`).
+- How to format seconds as `HH:MM:SS`.
+- The lap-time pattern.
+- How to bind keyboard shortcuts to Tkinter buttons.
## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
-- tkinter module (usually comes pre-installed)
+- Python 3.6 or above.
+- A text editor or IDE.
+- Tkinter (ships with Python; Linux: `sudo apt install python3-tk`).
+- Familiarity with the Tkinter event loop. [Basic Music Player](./basicmusicplayer) is a softer first GUI project.
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download).
+## Getting Started
-Note: This project uses only built-in Python modules, so no additional installations are required.
+### Create the project
+1. Create folder `simple-stopwatch`.
+2. Inside, create `simplestopwatch.py`.
-## Getting Started
-#### Create a Project
-1. Create a folder named `simple-stopwatch`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `simplestopwatch.py`.
-4. Copy the given code and paste it in your `simplestopwatch.py` file.
-
-## Write the Code
-1. Copy and paste the following code in your `simplestopwatch.py` file.
+### Write the code
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `simple-stopwatch`.
-```cmd title="command" showLineNumbers{1} {1-10}
+
+### Run it
+```cmd title="command"
C:\Users\Your Name\simple-stopwatch> python simplestopwatch.py
-# A GUI window will open with:
-# - Large digital time display showing "0"
-# - Start button to begin counting
-# - Stop button to pause counting
-# - Reset button to return to zero
-# - Black background with white text for better visibility
+# Window with big "0", Start / Stop / Reset buttons
```
-## Explanation
+## Step-by-Step Explanation
-### Code Breakdown
-1. **Import required modules.**
-```python title="simplestopwatch.py" showLineNumbers{1} {1-3}
-import time
-import datetime
+### 1. Imports and root window
+```python title="simplestopwatch.py"
import tkinter as tk
-```
-
-2. **Set up the main window.**
-```python title="simplestopwatch.py" showLineNumbers{1} {1-6}
root = tk.Tk()
root.title("Simple Stopwatch")
-root.geometry("500x500")
-root.resizable(False, False)
+root.geometry("400x300")
root.config(bg="black")
```
-3. **Define the start function.**
-```python title="simplestopwatch.py" showLineNumbers{1} {1-6}
-def start():
- global running
- running = True
- global count
- count = -1
- counter()
+### 2. State and label
+```python title="simplestopwatch.py"
+running = False
+count = 0
+label = tk.Label(root, text="0", font=("Helvetica", 80), bg="black", fg="white")
+label.pack(pady=20)
```
-4. **Implement the counter mechanism.**
-```python title="simplestopwatch.py" showLineNumbers{1} {1-8}
-def counter():
- global running
+### 3. The naïve counter loop
+```python title="simplestopwatch.py"
+def tick():
global count
if running:
count += 1
- time_label.config(text=str(count))
- time_label.after(1000, counter)
-```
+ label.config(text=str(count))
+ label.after(1000, tick) # schedule the next call
+
+def start():
+ global running
+ if not running:
+ running = True
+ tick()
-5. **Define stop and reset functions.**
-```python title="simplestopwatch.py" showLineNumbers{1} {1-8}
def stop():
global running
running = False
-
+
def reset():
global count
count = 0
- time_label.config(text=str(count))
+ label.config(text="0")
```
+`after(ms, fn)` tells Tkinter "call `fn` after `ms` milliseconds during the event loop." That is how you do "do something every second" without blocking the UI.
-6. **Create and place GUI widgets.**
-```python title="simplestopwatch.py" showLineNumbers{1} {1-10}
-time_label = tk.Label(root, text="0", font=("Helvetica", 80), bg="black", fg="white")
-start_button = tk.Button(root, text="Start", font=("Helvetica", 20), bg="black", fg="white", command=start)
-stop_button = tk.Button(root, text="Stop", font=("Helvetica", 20), bg="black", fg="white", command=stop)
-reset_button = tk.Button(root, text="Reset", font=("Helvetica", 20), bg="black", fg="white", command=reset)
-
-time_label.pack(pady=20)
-start_button.pack(pady=20)
-stop_button.pack(pady=20)
-reset_button.pack(pady=20)
-```
+### 4. Wire the buttons
+```python title="simplestopwatch.py"
+for txt, cmd in [("Start", start), ("Stop", stop), ("Reset", reset)]:
+ tk.Button(root, text=txt, font=("Helvetica", 18),
+ bg="black", fg="white", command=cmd).pack(pady=10)
-## Features
-- **Digital Display:** Large, easy-to-read time counter
-- **Start/Stop Functionality:** Control timing with button clicks
-- **Reset Capability:** Return counter to zero instantly
-- **Clean Interface:** Black background with white text for clarity
-- **Fixed Window Size:** Non-resizable window for consistent layout
-- **Real-time Updates:** Counter updates every second automatically
-- **State Management:** Proper handling of running/stopped states
-
-## How It Works
-
-### State Management
-The stopwatch uses global variables to manage its state:
-- **`running`:** Boolean flag indicating if the stopwatch is active
-- **`count`:** Integer storing the current elapsed seconds
-
-### Timer Mechanism
-The application uses Tkinter's `after()` method for timing:
-1. **Start:** Sets `running` to True and begins the counter
-2. **Counter:** Recursively calls itself every 1000ms (1 second)
-3. **Stop:** Sets `running` to False, breaking the recursive loop
-4. **Reset:** Sets count back to 0 and updates display
-
-### Event Flow
-```
-Start Button → running = True → counter() begins
- ↓
-counter() → updates display → schedules next call (1 second)
- ↓
-Stop Button → running = False → counter() stops recurring
- ↓
-Reset Button → count = 0 → display updates to "0"
+root.mainloop()
```
-## GUI Components
+## The Drift Problem
-### Time Display
-- **Font:** Helvetica, size 80 for maximum readability
-- **Colors:** White text on black background
-- **Position:** Centered at the top with padding
+The naïve version is wrong. Two reasons:
-### Control Buttons
-- **Start Button:** Begins or resumes timing
-- **Stop Button:** Pauses timing (can be resumed)
-- **Reset Button:** Resets counter to zero
-- **Styling:** Consistent black/white theme
+1. **Tkinter's `after` is not a precise scheduler.** A second's worth of wall-clock might be 1.01s on a busy laptop. After a minute you have drifted by half a second.
+2. **`count` measures "ticks fired", not elapsed time.** If the UI is sluggish, ticks pile up — or skip.
-## Use Cases
-- **Exercise Timing:** Track workout durations
-- **Study Sessions:** Monitor focused work periods
-- **Cooking:** Time preparation and cooking processes
-- **Presentations:** Keep track of speaking time
-- **General Timing:** Any activity requiring time measurement
+Fix it by recording a **start timestamp** and computing elapsed time from `time.time()`:
-## Next Steps
-You can enhance this project by:
-- Adding millisecond precision for more accurate timing
-- Implementing lap time functionality
-- Adding sound alerts at specific intervals
-- Creating multiple simultaneous timers
-- Adding time formatting (MM:SS or HH:MM:SS)
-- Implementing countdown timer mode
-- Adding keyboard shortcuts for controls
-- Creating customizable themes and colors
-- Adding time logging and history features
-- Implementing export functionality for recorded times
-
-## Enhanced Version Ideas
-```python title="simplestopwatch.py" showLineNumbers{1}
-def enhanced_stopwatch():
- # Features to add:
- # - Lap time recording
- # - Multiple time formats
- # - Sound notifications
- # - Customizable themes
- # - Keyboard shortcuts
- # - Time export functionality
- pass
-
-def format_time(seconds):
- # Convert seconds to HH:MM:SS format
- hours = seconds // 3600
- minutes = (seconds % 3600) // 60
- secs = seconds % 60
- return f"{hours:02d}:{minutes:02d}:{secs:02d}"
+```python title="fixed.py"
+import time
+start_time = 0.0
+elapsed_at_pause = 0.0
+running = False
+
+def tick():
+ if running:
+ elapsed = (time.time() - start_time) + elapsed_at_pause
+ label.config(text=format_time(elapsed))
+ label.after(50, tick) # 50 ms = 20 fps; smooth display, irrelevant to accuracy
+
+def start():
+ global running, start_time
+ if not running:
+ running = True
+ start_time = time.time()
+ tick()
+
+def stop():
+ global running, elapsed_at_pause
+ if running:
+ elapsed_at_pause += time.time() - start_time
+ running = False
+
+def reset():
+ global running, elapsed_at_pause
+ running = False
+ elapsed_at_pause = 0
+ label.config(text="00:00.0")
```
+Now the displayed time is always accurate to within one frame, regardless of how laggy the UI is.
-## Advanced Features Ideas
+## Format Time as HH:MM:SS.t
+
+```python title="format.py"
+def format_time(seconds: float) -> str:
+ h, rem = divmod(int(seconds), 3600)
+ m, s = divmod(rem, 60)
+ tenths = int((seconds - int(seconds)) * 10)
+ return f"{h:02d}:{m:02d}:{s:02d}.{tenths}"
+```
+`divmod(a, b)` returns `(a // b, a % b)` in one call — the standard Python idiom for time-component math.
+
+## Lap Times
+
+A lap button records the current elapsed time without resetting:
+```python title="laps.py"
+laps: list[float] = []
+laps_list = tk.Listbox(root, height=8)
+laps_list.pack(fill="x", padx=20)
-### Lap Time Functionality
-```python title="simplestopwatch.py" showLineNumbers{1}
def add_lap():
- global count
- lap_times.append(count)
- update_lap_display()
+ if not running: return
+ elapsed = (time.time() - start_time) + elapsed_at_pause
+ laps.append(elapsed)
+ n = len(laps)
+ diff = elapsed - (laps[-2] if n > 1 else 0)
+ laps_list.insert("end", f"Lap {n}: {format_time(elapsed)} (+{format_time(diff)})")
+
+tk.Button(root, text="Lap", command=add_lap).pack(pady=4)
+```
+Each entry shows both the total elapsed and the **split** from the previous lap.
+
+## Keyboard Shortcuts
-def show_lap_times():
- # Display list of recorded lap times
- pass
+Make Space toggle, R reset, L lap:
+```python title="keys.py"
+def toggle(_event=None):
+ stop() if running else start()
+
+root.bind("", toggle)
+root.bind("", lambda e: reset())
+root.bind("", lambda e: add_lap())
```
-### Time Formatting
-```python title="simplestopwatch.py" showLineNumbers{1}
-def format_display(seconds):
- if seconds < 60:
- return f"{seconds}s"
- elif seconds < 3600:
- minutes = seconds // 60
- secs = seconds % 60
- return f"{minutes:02d}:{secs:02d}"
- else:
- hours = seconds // 3600
- minutes = (seconds % 3600) // 60
- secs = seconds % 60
- return f"{hours:02d}:{minutes:02d}:{secs:02d}"
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| Drift after 5 minutes | Counted ticks, not elapsed time | Use `time.time()` reference |
+| Multiple `tick()` chains spawn after rapid Start clicks | No guard on `start()` | `if not running:` before scheduling |
+| Window freezes on heavy work | Did blocking work in callback | Keep callbacks fast; use threads for blocking ops |
+| Reset while running leaves stale tick scheduled | Did not stop the chain | Set `running = False` before resetting |
+| Tenths jitter visibly | Refreshed every 1000 ms | Refresh every 50–100 ms; format with fractional seconds |
+| Numbers misaligned | Variable-width font | Use a monospace font (`Consolas`, `Courier`) |
+
+## Variations to Try
+
+### 1. Countdown timer
+```python title="countdown.py"
+duration = 5 * 60 # 5 minutes
+def tick():
+ remaining = duration - ((time.time() - start_time) + elapsed_at_pause)
+ if remaining <= 0:
+ label.config(text="00:00.0")
+ notify("Time!")
+ return
+ label.config(text=format_time(remaining))
+ label.after(50, tick)
+```
+Combines neatly with [Basic Alarm Clock](./basicalarmclock).
+
+### 2. Pomodoro mode
+25 minutes of work, 5 minutes break, repeat. Toggle a `phase` state and chain timers.
+
+### 3. Save lap history
+JSON file with timestamp + laps list. Reload on next launch.
+
+### 4. CSV export
+"Export laps" button writes to `laps.csv`.
+
+### 5. Multiple stopwatches
+A `Tab` per stopwatch using `ttk.Notebook`. Each tab keeps its own state.
+
+### 6. Color states
+Green while running, red while stopped, blue at zero. Visual at a glance.
+
+### 7. Sound on lap / finish
+`winsound` on Windows, `playsound` cross-platform. See [Basic Music Player](./basicmusicplayer).
+
+### 8. Voice commands
+`pip install SpeechRecognition` — say "start", "stop", "lap", "reset".
+
+### 9. Race mode
+Two stopwatches in one window, each with its own start/stop button. Show who is ahead.
+
+### 10. Web stopwatch
+Flask plus a tiny page that updates with JS. The server keeps state per session.
+
+## Class-Based Refactor
+
+Globals are fine here, but if you build the multi-stopwatch variant you want state per instance:
+```python title="oop.py"
+class Stopwatch:
+ def __init__(self, parent):
+ self.running = False
+ self.elapsed = 0.0
+ self.start_time = 0.0
+ self.label = tk.Label(parent, text="00:00.0",
+ font=("Consolas", 60), bg="black", fg="white")
+ self.label.pack()
+ def start(self):
+ if not self.running:
+ self.running = True
+ self.start_time = time.time()
+ self._tick()
+ def _tick(self):
+ if not self.running: return
+ now_elapsed = (time.time() - self.start_time) + self.elapsed
+ self.label.config(text=format_time(now_elapsed))
+ self.label.after(50, self._tick)
+ def stop(self):
+ if self.running:
+ self.elapsed += time.time() - self.start_time
+ self.running = False
+ def reset(self):
+ self.running = False
+ self.elapsed = 0
+ self.label.config(text="00:00.0")
```
-## Performance Considerations
-- **Memory Usage:** Minimal memory footprint with simple variables
-- **CPU Usage:** Low impact with 1-second intervals
-- **GUI Responsiveness:** Non-blocking timer implementation
-- **Accuracy:** Good for general timing (±1 second precision)
+## Real-World Applications
+- **Workout / interval training**.
+- **Cooking** — multi-timer for parallel dishes.
+- **Productivity** — Pomodoro and time-tracking.
+- **Sports** — race day timing.
+- **Lab experiments** — recording multiple event durations.
+- **Billing** — track time on tasks.
## Educational Value
-This project teaches:
-- **GUI Development:** Creating interactive desktop applications
-- **Event Handling:** Responding to button clicks and timer events
-- **State Management:** Managing application state with global variables
-- **Timer Programming:** Using `after()` for scheduled updates
-- **Layout Management:** Organizing GUI components with pack()
-
-## Common Improvements
-- **Precision:** Use `time.time()` for sub-second accuracy
-- **Visual Feedback:** Add color changes for different states
-- **User Experience:** Add confirmation dialogs for reset
-- **Accessibility:** Implement keyboard navigation
-- **Persistence:** Save timing sessions to files
+- **Tkinter event loop & `after`** — the cornerstone of any GUI animation.
+- **Time accuracy** — counters drift, timestamps do not.
+- **State management** — `running`, `start_time`, `elapsed_at_pause`.
+- **Formatting** — `divmod` is the right tool for time math.
+- **Keyboard bindings** — usability beyond mouse clicks.
-## Real-World Applications
-- **Sports Training:** Track exercise and rest intervals
-- **Productivity:** Implement Pomodoro technique timing
-- **Education:** Time quiz sessions and study periods
-- **Professional:** Track billable work hours
-- **Entertainment:** Game timing and challenges
-
-## Troubleshooting Tips
-- **Timer Not Starting:** Check if `running` variable is properly set
-- **Display Not Updating:** Ensure `after()` method is called correctly
-- **Button Responsiveness:** Verify command functions are properly defined
-- **Window Issues:** Check geometry and resizable settings
+## Next Steps
+- Rewrite using **`time.time()`** for accuracy.
+- Add **`HH:MM:SS.t`** display.
+- Add **lap times** with a Listbox.
+- Add **keyboard shortcuts**.
+- Refactor into a **`Stopwatch` class** and host two of them in one window.
+- Pair with [Basic Alarm Clock](./basicalarmclock) for a countdown / Pomodoro hybrid.
## Conclusion
-In this project, we learned how to create a Simple Stopwatch using Python's Tkinter library. We explored GUI development concepts including event handling, timer programming, and state management. This project demonstrates how to create responsive, real-time applications with clean user interfaces. The stopwatch serves as an excellent foundation for understanding time-based applications and can be extended with many additional features. To find more projects like this, you can visit Python Central Hub.
+You built a real desktop stopwatch, then fixed the most common time-tracking bug (drift from counting ticks). The same lesson — measure *moments*, not *steps* — applies to nearly every time-sensitive program you will ever write. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/simplestopwatch.py). More GUI projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/tempconv.mdx b/src/content/docs/projects/Beginners/tempconv.mdx
index 02c7e121..791fc57f 100644
--- a/src/content/docs/projects/Beginners/tempconv.mdx
+++ b/src/content/docs/projects/Beginners/tempconv.mdx
@@ -1,6 +1,6 @@
---
title: Temperature Converter
-description: This program converts temperature from Fahrenheit to Celsius and vice versa. This program is written in Python. This is a command line program.
+description: Build a robust Celsius / Fahrenheit / Kelvin / Rankine converter in Python. Learn functions, conversion math, input validation, menu-driven UIs, and how to grow a single-formula script into a complete tool.
sidebar:
order: 4
hero:
@@ -13,29 +13,50 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-This program converts temperature from Fahrenheit to Celsius and vice versa. This program is written in Python. This is a command line program. This application work on the following formulae:
+Converting between temperature scales is one of the cleanest beginner projects: the math is simple, the use case is real, and there is plenty of room to grow the program. In this tutorial you will build a command-line temperature converter that handles Celsius and Fahrenheit at first, then extend it to Kelvin and Rankine, validate user input, and finish with a refactor that uses a dictionary-driven design — the same pattern professional codebases use to keep menus maintainable.
-#### `Celsius = (Fahrenheit - 32) * 5/9`
+By the end you will understand:
+- The four major temperature scales and how they relate.
+- Why `float` is preferred over `int` for measurements.
+- How to organize a script with small, single-purpose functions.
+- How to validate input so the program never crashes.
+- How a dispatch table replaces growing `if`/`elif` chains.
-#### `Fahrenheit = (Celsius * 9/5) + 32`
+## The Math
+
+| From → To | Formula |
+|---|---|
+| **Celsius → Fahrenheit** | `F = C × 9/5 + 32` |
+| **Fahrenheit → Celsius** | `C = (F − 32) × 5/9` |
+| **Celsius → Kelvin** | `K = C + 273.15` |
+| **Kelvin → Celsius** | `C = K − 273.15` |
+| **Fahrenheit → Kelvin** | `K = (F − 32) × 5/9 + 273.15` |
+| **Kelvin → Rankine** | `R = K × 9/5` |
+| **Rankine → Fahrenheit** | `F = R − 459.67` |
+
+**Reference points:**
+- Water freezes at 0 °C / 32 °F / 273.15 K.
+- Water boils at 100 °C / 212 °F / 373.15 K.
+- Absolute zero is 0 K / −273.15 °C / −459.67 °F / 0 °R.
## Prerequisites
-- Python 3.6 or above
-- Text Editor or IDE
+- Python 3.6 or above.
+- A text editor or IDE (VS Code recommended).
+- Comfort running a Python file from the terminal (see [Hello World](./helloworld)).
+- Basic understanding of functions (see [Simple Calculator](./simplecalculator) if you need a refresher).
## Getting Started
-### Creating a new project
-1. Create a new project folder and name it `tempconverter`.
-2. Create a new file inside the `tempconverter` folder and name it `tempconverter.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into the `tempconverter.py` file.
+
+### Create the project
+1. Create a folder named `tempconverter`.
+2. Inside, create `tempconverter.py`.
+3. Open the folder in your code editor.
### Write the code
-1. Add the following code to the `tempconverter.py` file.
+Add this to `tempconverter.py`:
-2. Save the file.
-3. Open the terminal and navigate to the project folder.
-4. Run the following command to execute the program.
+
+Save the file, open a terminal, and run:
```cmd title="command" showLineNumbers{1}
C:\Users\username\Documents\tempconverter> python tempconverter.py
Temperature Converter
@@ -43,59 +64,210 @@ Temperature Converter
2. Fahrenheit to Celsius
Enter your choice: 1
Enter temperature in Celsius: 35
-Temperature in Fahrenheit: 95.0
+Temperature in Fahrenheit: 95.0
```
-## Explanation
-- The `celsius_to_fahrenheit()` function takes a temperature in Celsius as an argument and returns the temperature in Fahrenheit.
-- The `fahrenheit_to_celsius()` function takes a temperature in Fahrenheit as an argument and returns the temperature in Celsius.
-- The `main()` function displays a menu to the user and asks the user to enter a choice. If the user enters 1, the program asks the user to enter a temperature in Celsius and calls the `celsius_to_fahrenheit()` function to convert the temperature from Celsius to Fahrenheit. If the user enters 2, the program asks the user to enter a temperature in Fahrenheit and calls the `fahrenheit_to_celsius()` function to convert the temperature from Fahrenheit to Celsius. If the user enters any other number, the program displays an error message.
-- The `main()` function is called at the end of the program.
+## Step-by-Step Explanation
-## Usage
-1. Open the terminal and navigate to the project folder.
-2. Run the following command to execute the program.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\tempconverter> python tempconverter.py
-Temperature Converter
-1. Celsius to Fahrenheit
-2. Fahrenheit to Celsius
-Enter your choice: 1
-Enter temperature in Celsius: 35
-Temperature in Fahrenheit: 95.0
+### 1. Define each conversion as a function
+```python title="tempconverter.py"
+def celsius_to_fahrenheit(c):
+ return c * 9/5 + 32
+
+def fahrenheit_to_celsius(f):
+ return (f - 32) * 5/9
```
-In this example, the user enters 1 to convert temperature from Celsius to Fahrenheit. The user enters 35 as the temperature in Celsius. The program calls the `celsius_to_fahrenheit()` function to convert the temperature from Celsius to Fahrenheit and displays the result.
-3. You can also use the program to convert temperature from Fahrenheit to Celsius.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\tempconverter> python tempconverter.py
-Temperature Converter
-1. Celsius to Fahrenheit
-2. Fahrenheit to Celsius
-Enter your choice: 2
-Enter temperature in Fahrenheit: 95
-Temperature in Celsius: 35.0
+Each function does **one** thing. The body is the formula directly. Naming functions after what they do makes the program readable without comments.
+
+### 2. The `main()` menu
+```python title="tempconverter.py"
+def main():
+ print("Temperature Converter")
+ print("1. Celsius to Fahrenheit")
+ print("2. Fahrenheit to Celsius")
+ choice = input("Enter your choice: ")
+ if choice == "1":
+ c = float(input("Enter temperature in Celsius: "))
+ print("Temperature in Fahrenheit:", celsius_to_fahrenheit(c))
+ elif choice == "2":
+ f = float(input("Enter temperature in Fahrenheit: "))
+ print("Temperature in Celsius:", fahrenheit_to_celsius(f))
+ else:
+ print("Invalid choice.")
```
-In this example, the user enters 2 to convert temperature from Fahrenheit to Celsius. The user enters 95 as the temperature in Fahrenheit. The program calls the `fahrenheit_to_celsius()` function to convert the temperature from Fahrenheit to Celsius and displays the result.
-4. You can also run the program by double-clicking the `tempconverter.py` file.
+- `input()` always returns a string.
+- `float()` turns that string into a number with a decimal — important because temperatures are rarely whole numbers.
-## Next Steps
-Congratulations! You have successfully created a temperature converter program in Python. You can now use this program to convert temperature from Celsius to Fahrenheit and vice versa.
-
-Here are some ideas to try:
-1. Modify the program to convert temperature from Kelvin to Celsius and vice versa.
-2. Modify the program to convert temperature from Kelvin to Fahrenheit and vice versa.
-3. Modify the program to convert temperature from Celsius to Kelvin and vice versa.
-4. Modify the program to convert temperature from Fahrenheit to Kelvin and vice versa.
-5. Modify the program to convert temperature from Kelvin to Rankine and vice versa.
-6. Modify the program to convert temperature from Rankine to Kelvin and vice versa.
-7. Modify the program to convert temperature from Celsius to Rankine and vice versa.
-8. Modify the program to convert temperature from Rankine to Celsius and vice versa.
-9. Modify the program to convert temperature from Fahrenheit to Rankine and vice versa.
-10. Modify the program to convert temperature from Rankine to Fahrenheit and vice versa.
+### 3. Run the program
+```python title="tempconverter.py"
+main()
+```
+Calling `main()` at the bottom of the file kicks the program off when you run `python tempconverter.py`.
-## Conclusion
-In this tutorial, you learned how to create a temperature converter program in Python. You also learned how to convert temperature from Celsius to Fahrenheit and vice versa. You can now use this program to convert temperature from Celsius to Fahrenheit and vice versa.
+## Why `float`, Not `int`?
+Try replacing `float(...)` with `int(...)`:
+
+```python title="why_float.py"
+int(input("Enter Celsius: ")) # user enters 36.6 → ValueError
+```
-You also learned how to use programming to perform calculations. You can use this knowledge to create more programs that perform calculations.
+`int()` refuses fractional input. Real temperatures (body temperature 36.6 °C, room temperature 22.5 °C) are not whole numbers. `float()` accepts both `36` and `36.6`.
+
+## Robust Input
+
+The naive version crashes when the user types `"hot"` instead of a number. Add a helper:
+
+```python title="ask_number.py"
+def ask_number(prompt):
+ while True:
+ raw = input(prompt)
+ try:
+ return float(raw)
+ except ValueError:
+ print(f"'{raw}' is not a valid number. Try again.")
+```
+Use it everywhere you would have written `float(input(...))`:
+```python title="usage.py"
+c = ask_number("Enter temperature in Celsius: ")
+```
+
+You can also reject obvious physics nonsense — no temperature below absolute zero:
+```python title="phys_check.py"
+def celsius_to_kelvin(c):
+ if c < -273.15:
+ raise ValueError("Temperature below absolute zero is impossible.")
+ return c + 273.15
+```
-Keep learning and keep building awesome stuff. If you have any questions or feedback, please feel free to send me a message on [Website](https://ravikishan.me/contact). I would love to hear from you. Learn more about tutorial and projects on Python Central Hub.
\ No newline at end of file
+## Refactor: Dispatch Table
+
+When you support seven or eight conversions, a long `if`/`elif` chain becomes painful. Replace it with a dictionary that maps menu labels to conversion functions:
+
+```python title="dispatch.py"
+def c_to_f(c): return c * 9/5 + 32
+def f_to_c(f): return (f - 32) * 5/9
+def c_to_k(c): return c + 273.15
+def k_to_c(k): return k - 273.15
+def f_to_k(f): return c_to_k(f_to_c(f))
+def k_to_f(k): return c_to_f(k_to_c(k))
+
+CONVERSIONS = {
+ "1": ("Celsius → Fahrenheit", c_to_f, "°C", "°F"),
+ "2": ("Fahrenheit → Celsius", f_to_c, "°F", "°C"),
+ "3": ("Celsius → Kelvin", c_to_k, "°C", "K"),
+ "4": ("Kelvin → Celsius", k_to_c, "K", "°C"),
+ "5": ("Fahrenheit → Kelvin", f_to_k, "°F", "K"),
+ "6": ("Kelvin → Fahrenheit", k_to_f, "K", "°F"),
+}
+
+def main():
+ while True:
+ print("\nTemperature Converter")
+ for key, (label, _, _, _) in CONVERSIONS.items():
+ print(f" {key}. {label}")
+ print(" Q. Quit")
+ choice = input("Choice: ").strip()
+ if choice.lower() == "q":
+ break
+ if choice not in CONVERSIONS:
+ print("Invalid choice.")
+ continue
+ label, func, in_unit, out_unit = CONVERSIONS[choice]
+ value = float(input(f"Enter temperature ({in_unit}): "))
+ print(f"{value} {in_unit} = {func(value):.2f} {out_unit}")
+
+main()
+```
+Notice how easy it is to add a new conversion — one entry in the dictionary, one helper function. No menu logic has to change.
+
+## Output Formatting
+
+`print(c_to_f(35))` prints `95.0`. For two decimal places:
+```python title="fmt.py"
+print(f"{c_to_f(35):.2f}") # 95.00
+```
+The `:.2f` inside the f-string means "format as a float with two digits after the decimal point."
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `TypeError: unsupported operand type(s) for *: 'str' and 'float'` | Forgot to convert `input()` with `float()` | Wrap input in `float(input(...))` |
+| Wrong result like `0 + 32 = 32` for 0 °C → F (looks right but for 100 °C you get 132) | Used `c + 9/5 + 32` instead of `c * 9/5 + 32` | Watch operator precedence; parenthesize when unsure |
+| `ValueError: could not convert string to float: ''` | User pressed Enter without typing | Validate `raw` before converting |
+| Off-by-273 for Kelvin | Used 273 instead of 273.15 | Use the more accurate constant |
+
+## Variations to Try
+
+### 1. Convert multiple values at once
+Accept a comma-separated list:
+```python title="batch.py"
+raw = input("Celsius values, comma separated: ")
+for piece in raw.split(","):
+ c = float(piece.strip())
+ print(f"{c} °C = {c_to_f(c):.2f} °F")
+```
+
+### 2. CLI flags with argparse
+```python title="cli.py"
+import argparse
+parser = argparse.ArgumentParser()
+parser.add_argument("value", type=float)
+parser.add_argument("--from", dest="src", choices=["c","f","k"], required=True)
+parser.add_argument("--to", dest="dst", choices=["c","f","k"], required=True)
+args = parser.parse_args()
+# dispatch by (args.src, args.dst)
+```
+Now `python tempconverter.py 100 --from c --to f` returns `212.0`.
+
+### 3. GUI version
+Use Tkinter:
+```python title="gui.py"
+import tkinter as tk
+root = tk.Tk()
+entry = tk.Entry(root); entry.pack()
+result = tk.Label(root); result.pack()
+def convert():
+ c = float(entry.get())
+ result.config(text=f"{c_to_f(c):.2f} °F")
+tk.Button(root, text="Convert", command=convert).pack()
+root.mainloop()
+```
+
+### 4. Weather-API integration
+Use `requests` to fetch the current temperature for a city from an API like OpenWeatherMap, then convert and display.
+
+### 5. Unit tests
+```python title="test_conv.py"
+import unittest
+class TestConv(unittest.TestCase):
+ def test_freezing(self):
+ self.assertEqual(c_to_f(0), 32)
+ def test_boiling(self):
+ self.assertEqual(c_to_f(100), 212)
+unittest.main()
+```
+Run with `python test_conv.py`.
+
+## Real-World Applications
+- Weather and forecasting tools.
+- Cooking apps that swap between °C and °F for international recipes.
+- Industrial monitoring dashboards that use Kelvin internally and display in °C.
+- Scientific instruments that report in Kelvin or Rankine.
+- Education — building physical intuition for the scales.
+
+## Best Practices Demonstrated
+- **Single-purpose functions** — each conversion has its own function with a clear name.
+- **Input validation at the boundary** — convert and verify once, trust the value everywhere else.
+- **Dispatch table over `if`/`elif` ladders** — easier to maintain.
+- **Format output explicitly** — `:.2f` produces predictable, readable numbers.
+
+## Next Steps
+- Add Rankine support so the converter handles all four standard scales.
+- Build a GUI with Tkinter for a nicer experience.
+- Add a "smart" mode that auto-detects the unit by suffix (`100C`, `212F`, `300K`).
+- Publish it as a PyPI package called `tempconv`.
+- Pair it with a weather API for live-temperature conversion of any city.
+
+## Conclusion
+Temperature conversion is the perfect playground for practicing functions, math operators, user input, validation, and code refactoring. You started with a 10-line script and finished with a dispatch-table design that scales cleanly. The same dictionary-of-functions pattern shows up in unit converters, command-line tools, and event handlers throughout real-world Python projects. The full source is on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/tempconverter.py). Find more beginner projects on Python Central Hub — and if you have questions, reach out at [ravikishan.me/contact](https://ravikishan.me/contact).
diff --git a/src/content/docs/projects/Beginners/textbasedadventuregame.mdx b/src/content/docs/projects/Beginners/textbasedadventuregame.mdx
index d87399b2..ed77c0c2 100644
--- a/src/content/docs/projects/Beginners/textbasedadventuregame.mdx
+++ b/src/content/docs/projects/Beginners/textbasedadventuregame.mdx
@@ -1,6 +1,6 @@
---
title: Text-Based Adventure Game
-description: Create an interactive story-driven adventure game with choices, inventory system, and branching narratives
+description: Build a branching text adventure in Python with inventory, choices, multiple endings, and a save system. Learn state machines, narrative design, JSON-driven scenes, and how interactive fiction engines actually work.
sidebar:
order: 47
hero:
@@ -14,38 +14,41 @@ import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Build an engaging text-based adventure game featuring a fantasy story, player choices, inventory management, and multiple endings. This project demonstrates game logic, story branching, state management, and interactive narrative design using only text output and user input.
+Text adventures — *Colossal Cave Adventure*, *Zork*, *Hitchhiker's Guide* — were the first true interactive fiction. They are also one of the best teaching projects in programming because they force you to think about **state** (what the player has, where they are, what they have done) and **flow** (which scene leads to which, under which conditions). In this project you build a fantasy adventure with a cave, a house, a sword, a wicked fairy, multiple endings, and a replay system. Then we refactor it from hard-coded scenes into a JSON-driven engine — the same architecture commercial interactive fiction tools use.
-## Prerequisites
+You will learn:
+- How to organize a game as a **finite state machine** of scenes.
+- How to manage **inventory** as a list of strings.
+- How to validate player input cleanly with a `while not valid` loop.
+- How to add **save/load** so a player can resume later.
+- How to extract the story from the code into a data file you can edit without programming.
-- Python 3.6 or above
-- Text Editor or IDE
-- Basic understanding of Python syntax
-- Knowledge of functions and control structures
-- Familiarity with lists and string manipulation
-- Understanding of game logic and state management
+## Prerequisites
+- Python 3.6 or above.
+- A text editor or IDE.
+- Comfort with functions, conditionals, and lists.
+- (Bonus) Familiarity with dictionaries and JSON for the engine refactor.
## Getting Started
-### Create a new project
-1. Create a new project folder and name it `textBasedAdventureGame`.
-2. Create a new file and name it `textbasedadventuregame.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into your `textbasedadventuregame.py` file.
+### Create the project
+1. Create a folder named `text-adventure`.
+2. Inside it, create `textbasedadventuregame.py`.
### Write the code
-1. Add the following code to your `textbasedadventuregame.py` file.
-
-2. Save the file.
-3. Run the following command to run the application.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\textBasedAdventureGame> python textbasedadventuregame.py
+
+
+### Run it
+```bash title="run"
+python textbasedadventuregame.py
+```
+
+```
You find yourself standing in front of a cave.
In your hand you hold your trusty (but not very effective) dagger.
Enter 1 to knock on the door of the house.
Enter 2 to peer into the cave.
-Please enter 1 or 2.
2
You have found the magical Sword of Ogoroth!
@@ -54,158 +57,253 @@ You are victorious!
Would you like to play again? (y/n)
```
-3. **Play the Game**
- - Read the story text carefully
- - Make choices by entering numbers (1 or 2)
- ```
-
-## Explanation
-
-1. The `import time` statement imports the time module for adding pauses between story elements.
-2. The `def print_pause()` function adds dramatic delays to create better story pacing.
-3. The `def intro():` function sets up the initial game world and story context.
-4. The `items = []` list tracks the player's inventory throughout the game.
-5. The `def cave(items):` function handles the cave exploration and item discovery.
-6. The `def house(items):` function manages the house encounter and combat.
-7. The story branches based on whether the player has found the sword.
-8. The `valid_inputs` list ensures players can only enter valid choices.
-9. The `while True:` loops handle input validation and choice repetition.
-10. The `def play_again():` function allows players to restart the adventure.
-11. The game demonstrates consequence-based gameplay with different outcomes.
-12. Multiple endings depend on player preparation and choices made.
-
-## Next Steps
-
-Congratulations! You have successfully created a Text-Based Adventure Game in Python. Experiment with the code and see if you can modify the application. Here are a few suggestions:
-- Add more locations and story branches
-- Implement character stats (health, strength, magic)
-- Create more complex inventory with multiple items
-- Add random events and encounters
-- Implement save/load game functionality
-- Create multiple characters to choose from
-- Add puzzle-solving elements
-- Implement dialogue trees with NPCs
-- Create a larger world map with navigation
-
-## Conclusion
-
-In this project, you learned how to create a Text-Based Adventure Game in Python. You also learned about story branching, state management, inventory systems, and creating interactive narratives. You can find the source code on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/textbasedadventuregame.py)
+## Step-by-Step Explanation
+### 1. Slow-print for drama
+```python title="textbasedadventuregame.py"
+import time
-## Code Explanation
-
-### Story Pacing and Immersion
-```python title="textbasedadventuregame.py" showLineNumbers{1}
-def print_pause(message_to_print):
- print(message_to_print)
- time.sleep(2)
+def print_pause(message, seconds=2):
+ print(message)
+ time.sleep(seconds)
```
+Without delays, the game prints its entire story in milliseconds and loses all sense of pacing. A short sleep after each message lets the player actually read.
-Adds dramatic pauses between story elements to create better pacing and immersion in the narrative.
-
-### Game Introduction
-```python title="textbasedadventuregame.py" showLineNumbers{1}
+### 2. Intro scene
+```python title="textbasedadventuregame.py"
def intro():
print_pause("You find yourself standing in an open field, filled with grass and yellow wildflowers.")
print_pause("Rumor has it that a wicked fairie is somewhere around here...")
print_pause("In your hand you hold your trusty (but not very effective) dagger.")
```
+Plain-text scene-setting. The narrative *is* the gameplay in text adventures, so the writing matters.
-Sets up the game world, story context, and initial player state with atmospheric descriptions.
+### 3. Scene with a choice
+```python title="textbasedadventuregame.py"
+def field(items):
+ print_pause("Enter 1 to knock on the door of the house.")
+ print_pause("Enter 2 to peer into the cave.")
+ while True:
+ choice = input("(Please enter 1 or 2.) ").strip()
+ if choice == "1":
+ house(items)
+ break
+ elif choice == "2":
+ cave(items)
+ break
+```
+The `while True` + `if/elif/break` pattern is the canonical way to handle constrained input. Each branch transitions to a different scene function — that is your state machine.
-### Inventory System
-```python title="textbasedadventuregame.py" showLineNumbers{1}
+### 4. Inventory drives outcomes
+```python title="textbasedadventuregame.py"
def cave(items):
if "Sword of Ogoroth" in items:
print_pause("You've been here before, and gotten all the good stuff.")
else:
print_pause("You have found the magical Sword of Ogoroth!")
items.append("Sword of Ogoroth")
+ field(items) # back to the field
```
+The `items` list is passed *through* every scene. Picking up an item is one `append` away. Branching on whether you have an item is one `in` check away.
-Implements item collection and inventory tracking that affects story outcomes and player capabilities.
+### 5. Combat that depends on preparation
+```python title="textbasedadventuregame.py"
+def house(items):
+ print_pause("You approach the door of the house.")
+ print_pause("From within you hear a sinister cackle. The wicked fairie is inside.")
+ print_pause("You must decide what to do.")
-### Branching Combat System
-```python title="textbasedadventuregame.py" showLineNumbers{1}
-if "Sword of Ogoroth" in items:
- print_pause("The Sword of Ogoroth shines brightly in your hand...")
- print_pause("You have rid the town of the wicked fairie. You are victorious!")
-else:
- print_pause("Your dagger is no match for the wicked fairie.")
- print_pause("You have been defeated!")
+ while True:
+ choice = input("(1) fight (2) run\n> ")
+ if choice == "1":
+ if "Sword of Ogoroth" in items:
+ print_pause("The Sword of Ogoroth shines brightly in your hand...")
+ print_pause("You have defeated the fairie. Victory!")
+ else:
+ print_pause("Your dagger is no match for the fairie.")
+ print_pause("You have been defeated.")
+ play_again()
+ break
+ elif choice == "2":
+ print_pause("You run back to the field.")
+ field(items)
+ break
```
+The same scene has two endings — one rewarding preparation, one punishing recklessness.
+
+### 6. Replay loop
+```python title="textbasedadventuregame.py"
+def play_again():
+ while True:
+ ans = input("Play again? (y/n) ").strip().lower()
+ if ans == "y":
+ play_game()
+ break
+ if ans == "n":
+ print_pause("Thanks for playing!")
+ quit()
+```
+`quit()` terminates the program cleanly when the player is done.
+
+### 7. The top-level driver
+```python title="textbasedadventuregame.py"
+def play_game():
+ items = []
+ intro()
+ field(items)
+
+play_game()
+```
+A fresh `items = []` per game guarantees a clean slate.
+
+## The State Machine View
-Creates different story outcomes based on player preparation and choices, demonstrating consequence-based gameplay.
+Even this small game is a graph:
+
+```
+intro → field ──> cave ─→ field (with sword)
+ ↓
+ house ──> fight ─→ (win|lose)
+ └─> run ─→ field
+```
+
+Every scene is a node. Every choice is an edge. As you add scenes, draw the graph on paper — it stops you writing dead-ends or unreachable scenes.
+
+## Add Save / Load
+
+Players appreciate not losing progress.
+
+```python title="save.py"
+import json
+from pathlib import Path
+
+def save_game(items, location):
+ Path("save.json").write_text(json.dumps({"items": items, "location": location}))
+
+def load_game():
+ if not Path("save.json").exists():
+ return None
+ return json.loads(Path("save.json").read_text())
+```
+Each scene gets a name. The current scene name and the inventory are all that need persisting in a simple game.
+
+## Refactor: Data-Driven Engine
+
+Hard-coded scenes do not scale past 10 nodes. A real engine treats scenes as **data**:
+
+```python title="scenes.json"
+{
+ "field": {
+ "text": "You stand in an open field. A house sits to your left and a cave to your right.",
+ "choices": [
+ { "label": "Knock on the house door", "go": "house" },
+ { "label": "Peer into the cave", "go": "cave" }
+ ]
+ },
+ "cave": {
+ "text": "You find the magical Sword of Ogoroth!",
+ "give": "sword",
+ "choices": [{ "label": "Return to the field", "go": "field" }]
+ },
+ "house": {
+ "text": "A wicked fairy lives here.",
+ "choices": [
+ { "label": "Fight", "go": "victory", "require": "sword" },
+ { "label": "Fight", "go": "defeat", "deny": "sword" },
+ { "label": "Run", "go": "field" }
+ ]
+ },
+ "victory": { "text": "You won!", "end": true },
+ "defeat": { "text": "You lost.", "end": true }
+}
+```
+
+The engine becomes tiny:
+```python title="engine.py"
+import json
+data = json.load(open("scenes.json"))
+state = {"location": "field", "items": []}
-### Choice Validation and Loops
-```python title="textbasedadventuregame.py" showLineNumbers{1}
while True:
- choice = input("Would you like to (1) fight or (2) run away?")
- if choice == "1":
- # Handle fight choice
- break
- if choice == "2":
- # Handle run choice
+ scene = data[state["location"]]
+ print(scene["text"])
+ if scene.get("give"):
+ state["items"].append(scene["give"])
+ if scene.get("end"):
break
+ options = [c for c in scene["choices"]
+ if c.get("require", None) in (None, *state["items"])
+ and c.get("deny", "__none__") not in state["items"]]
+ for i, c in enumerate(options, 1):
+ print(f" {i}. {c['label']}")
+ n = int(input("> ")) - 1
+ state["location"] = options[n]["go"]
```
+You now write stories in JSON. Game designers (or you, at 2 AM) can iterate on the narrative without touching Python.
-Ensures valid input from players and provides clear choice options with proper loop handling.
+## Common Mistakes
-### Replay System
-```python title="textbasedadventuregame.py" showLineNumbers{1}
-def play_again():
- while True:
- choice = input("Would you like to play again? (y/n)")
- if choice == "y":
- play_game()
- break
- elif choice == "n":
- print_pause("Thanks for playing! See you next time.")
- break
-```
+| Problem | Cause | Fix |
+|---|---|---|
+| Loop never exits | Forgot `break` after a choice handler | Always `break` after dispatching |
+| State leaks between runs | Globals reused | Build state inside `play_game()` |
+| Bad input crashes the game | `int(input())` without `try` | Validate, or only accept letters/specific strings |
+| Scenes call each other recursively forever | No clear terminal scene | Add explicit win/lose endings |
+| Long scenes blur together | No pacing | Add `time.sleep` and section breaks |
-Allows players to restart the adventure without relaunching the program.
+## Variations to Try
-## Features
+### 1. Health and damage stats
+A `health` attribute that decreases on bad choices and ends the game at 0.
-- **Immersive Storytelling**: Rich narrative with atmospheric descriptions
-- **Player Choices**: Multiple decision points affecting story outcomes
-- **Inventory System**: Item collection that changes gameplay possibilities
-- **Multiple Endings**: Different outcomes based on player preparation and choices
-- **Replayability**: Option to play again and try different strategies
-- **Input Validation**: Robust handling of user choices
-- **Progressive Revelation**: Story elements revealed based on player actions
+### 2. NPC dialogue
+A scene where the player chooses what to say; the NPC responds based on prior choices. Track `flags` like `met_wizard = True`.
-## Next Steps
+### 3. Random encounters
+With a 25 % chance, an enemy intercepts the player on a path. Use `random.random() < 0.25`.
+
+### 4. Map with directions
+Instead of numbered choices, accept `north`, `south`, `east`, `west`. Store the map as a dict of rooms with `exits`.
+
+### 5. Combat system
+Damage dice (`1d6 + str`), turn-based combat, escape attempts. See [Dice Rolling Simulator](./dicerolling) for the dice mechanic.
+
+### 6. Multiple character classes
+Warrior, mage, rogue — each starts with different equipment and stats; some scenes only unlock for specific classes.
-### Enhancements
-- Add more locations and story branches
-- Implement character stats (health, strength, magic)
-- Create more complex inventory with multiple items
-- Add random events and encounters
-- Include puzzle-solving elements
-- Implement save/load game functionality
-- Add character classes and skills
-- Create multiple story campaigns
-
-### Learning Extensions
-- Study advanced narrative design patterns
-- Explore procedural story generation
-- Learn about game state serialization
-- Practice with object-oriented game design
-- Understand random content generation
-- Explore text parsing for natural language input
+### 7. Save slots
+Multiple JSON files (`save1.json`, `save2.json`) plus a menu to pick one.
+
+### 8. Editor mode
+A separate Python script that lets a designer type scenes interactively and append them to `scenes.json`.
+
+### 9. Voice narration
+Pipe each `print_pause` line through `pyttsx3` to read aloud.
+
+### 10. Web port
+Wrap the engine in Flask. Each scene becomes a page; choices become links. The state lives in the user's session.
+
+## Real-World Applications
+- **Onboarding flows** — branching tutorials that adapt to user knowledge.
+- **Chatbots with structured paths** — customer-service trees.
+- **Educational simulations** — choose-your-own-adventure for ethics or training.
+- **Decision-tree visualizations** — the engine is a generic state machine.
+- **Game prototypes** — text mode for everything before art and engines.
## Educational Value
+- **State machines** — scenes and transitions are an explicit FSM.
+- **Inventory pattern** — applicable to shopping carts, permissions, feature flags.
+- **Branching logic** — what most "config-driven" systems boil down to.
+- **Refactoring from script to engine** — extracting data from code is a fundamental engineering skill.
+- **Narrative design** — pacing, payoff, agency.
-This project teaches:
-- **Game Logic Design**: Creating rules and systems for interactive experiences
-- **State Management**: Tracking player progress and inventory across game sessions
-- **Narrative Programming**: Implementing story branching and choice consequences
-- **Input Validation**: Handling user input robustly and providing clear feedback
-- **Function Organization**: Structuring code for complex interactive systems
-- **Flow Control**: Managing program flow through different story paths
-- **User Experience**: Creating engaging and intuitive text-based interfaces
-- **Story Design**: Balancing player agency with narrative structure
-
-Perfect for understanding game development fundamentals, interactive storytelling, and user-driven application design.
+## Next Steps
+- Add **`time.sleep` pacing** everywhere — it transforms the feel.
+- Extend with **save/load**.
+- Refactor to the **JSON-driven engine** above.
+- Write a longer story — 20+ scenes, 3 endings.
+- Try the **`textual`** library for a richer TUI with mouse and color support.
+- Port to Twine or Ink (industry-standard tools) once your story outgrows JSON.
+
+## Conclusion
+You built a complete branching adventure, learned to manage state and inventory, and saw the refactor path from hard-coded scenes to a real data-driven engine. The same patterns underlie every interactive fiction system, chatbot, and onboarding flow. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/textbasedadventuregame.py). Explore more game projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/todo.mdx b/src/content/docs/projects/Beginners/todo.mdx
index a4c19d1a..e31ad2f1 100644
--- a/src/content/docs/projects/Beginners/todo.mdx
+++ b/src/content/docs/projects/Beginners/todo.mdx
@@ -1,6 +1,6 @@
---
title: Todo List Application
-description: Build a comprehensive task management system with file persistence, CRUD operations, and command-line interface
+description: Build a CLI todo manager in Python with persistence, CRUD operations, priorities, due dates, search, and a path to SQLite, JSON, and Tkinter GUI versions.
sidebar:
order: 49
hero:
@@ -13,176 +13,286 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+A todo list is the canonical "first useful CLI app". You learn file I/O, input validation, CRUD (Create / Read / Update / Delete), confirmations for destructive operations, and the basic shape of a long-running interactive program. In this project you will build the text-file version, fix several subtle bugs in the naïve approach, then evolve toward a richer todo manager with priorities, due dates, search, JSON storage, SQLite storage, and finally a Tkinter GUI.
-Create a fully functional todo list application that allows users to add, view, delete, and manage tasks through a command-line interface. The application features persistent file storage, comprehensive task management operations, and user-friendly confirmation dialogs.
+You will leave comfortable with:
+- Reading and writing text files safely.
+- Persisting state between runs.
+- Implementing CRUD operations.
+- Designing menu-driven CLIs that do not rot as features pile up.
+- Choosing a storage format (text, JSON, SQLite) for the right job.
## Prerequisites
-
-- Python 3.6 or above
-- Text Editor or IDE
-- Basic understanding of Python syntax
-- Knowledge of file operations (read, write, append)
-- Familiarity with lists and string manipulation
-- Understanding of control structures and loops
+- Python 3.6 or above.
+- A text editor or IDE.
+- Comfort with functions, lists, and basic file I/O.
## Getting Started
-### Create a new project
-1. Create a new project folder and name it `todoListApp`.
-2. Create a new file and name it `todo.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into your `todo.py` file.
+### Create the project
+1. Create folder `todo-app`.
+2. Inside, create `todo.py`.
### Write the code
-1. Add the following code to your `todo.py` file.
-2. Save the file.
-3. Run the following command to run the application.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\todoListApp> python todo.py
+
+### Run it
+```cmd title="command"
+C:\Users\username\Documents\todo-app> python todo.py
----------TO DO LIST----------
1. Add task
2. Delete task
-3. Show task
-4. exit
-enter the choice: 1
-enter the task: Buy groceries
-Task Added Successfully.
+3. Show tasks
+4. Exit
+Enter the choice: 1
+Enter the task: Buy groceries
+Task added successfully.
+```
-enter the choice: 3
-1: Buy groceries
+## Step-by-Step Explanation
-enter the choice: 4
+### 1. Add a task
+```python title="todo.py"
+def add_task():
+ task = input("Enter task: ").strip()
+ if not task:
+ print("Empty task ignored.")
+ return
+ with open("todo.txt", "a", encoding="utf-8") as f:
+ f.write(task + "\n")
+ print("Task added.")
```
+Three things matter:
+- `"a"` mode **appends** instead of overwriting.
+- `with open(...)` guarantees the file is closed even on exception.
+- `encoding="utf-8"` makes the program work for non-English tasks.
-3. **Use the Application**
- - Choose operations from the main menu (1-4, E, R, H)
- - Add tasks with descriptive names
- - View your current task list
- - Delete individual tasks or clear all tasks
+### 2. View tasks
+```python title="todo.py"
+def view_tasks():
+ if not os.path.exists("todo.txt"):
+ print("No tasks yet.")
+ return
+ with open("todo.txt", encoding="utf-8") as f:
+ tasks = [line.rstrip("\n") for line in f if line.strip()]
+ if not tasks:
+ print("No tasks found.")
+ return
+ for i, t in enumerate(tasks, start=1):
+ print(f"{i}. {t}")
+```
+- `enumerate(..., start=1)` gives 1-based numbering for the user.
+- Empty lines are filtered out.
-## Code Explanation
+### 3. Delete a task
+```python title="todo.py"
+def delete_task():
+ tasks = read_tasks()
+ if not tasks:
+ print("Nothing to delete.")
+ return
+ show(tasks)
+ try:
+ n = int(input("Number to delete: "))
+ except ValueError:
+ print("Enter a number.")
+ return
+ if not 1 <= n <= len(tasks):
+ print("Out of range.")
+ return
+ removed = tasks.pop(n - 1)
+ write_tasks(tasks)
+ print(f"Deleted: {removed}")
+```
+Reading the whole file, mutating in memory, and writing it back is fine for tens or hundreds of tasks. SQLite becomes preferable past that.
-### File-Based Persistence
-```python title="todo.py" showLineNumbers{1}
-def add_task():
- task = input("Enter Task: ")
- with open("todo.txt", "a") as f:
- f.write(task + "\n")
- print("Task Added Successfully.")
+### 4. Confirm destructive operations
+```python title="todo.py"
+def delete_all():
+ if input("Are you sure? (y/n) ").lower() != "y":
+ print("Cancelled.")
+ return
+ open("todo.txt", "w").close()
+ print("All tasks deleted.")
```
+A `y/n` confirmation prevents accidental wipes.
-Uses text file storage to maintain tasks between application sessions, ensuring data persistence.
-
-### Task Viewing with Numbering
-```python title="todo.py" showLineNumbers{1}
-def view_task():
- with open("todo.txt", "r") as f:
- tasks = f.readlines()
- if len(tasks) == 0:
- print("No Tasks Found.")
- else:
- for i in range(len(tasks)):
- print(str(i + 1) + ". " + tasks[i].strip("\n"))
+### 5. The main loop
+```python title="todo.py"
+def main():
+ while True:
+ print("\n1. Add 2. Delete 3. View 4. Clear all 5. Exit")
+ choice = input("> ").strip()
+ if choice == "1": add_task()
+ elif choice == "2": delete_task()
+ elif choice == "3": view_tasks()
+ elif choice == "4": delete_all()
+ elif choice == "5": break
+ else: print("Invalid choice.")
```
-Displays tasks in a numbered list format for easy reference and selection.
+## Bugs in the Naïve Version
-### Safe Task Deletion
-```python title="todo.py" showLineNumbers{1}
-def delete_task():
- with open("todo.txt", "r") as f:
- tasks = f.readlines()
- # Display current tasks
- task_no = int(input("Enter Task Number to Delete: "))
- if task_no > len(tasks):
- print("Invalid Task Number.")
- else:
- del tasks[task_no - 1]
- with open("todo.txt", "w") as f:
- for task in tasks:
- f.write(task)
+The most common errors in beginner todo apps:
+1. **No `strip()` on read lines** — `"Buy bread\n"` and `"Buy bread"` look identical but are not equal.
+2. **`open(...)` without `with`** — leaks file descriptors on exception.
+3. **No empty-task check** — pressing Enter creates blank tasks that confuse `delete`.
+4. **No off-by-one guard** — `tasks[task_no]` instead of `tasks[task_no - 1]` deletes the wrong row.
+5. **No encoding** — the program breaks the moment someone adds an emoji or accented character.
+
+The version above fixes all five.
+
+## Add Priorities and Due Dates
+
+Plain text loses structure the moment you want anything richer than a string. Move to JSON:
+```python title="json_store.py"
+from pathlib import Path
+import json
+STORE = Path("todo.json")
+
+def read_tasks():
+ return json.loads(STORE.read_text(encoding="utf-8")) if STORE.exists() else []
+
+def write_tasks(tasks):
+ STORE.write_text(json.dumps(tasks, indent=2), encoding="utf-8")
+
+def add(title, priority="medium", due=None):
+ tasks = read_tasks()
+ tasks.append({"title": title, "priority": priority,
+ "due": due, "done": False})
+ write_tasks(tasks)
```
+Each task is a dict with `title`, `priority`, `due` (ISO date), and `done`. Now you can:
+- Filter by priority.
+- Sort by due date.
+- Mark tasks done without deleting them.
+- Show only open tasks.
+
+## Search and Filter
-Implements safe task removal with validation and immediate file updates.
+```python title="filter.py"
+def search(keyword):
+ return [t for t in read_tasks() if keyword.lower() in t["title"].lower()]
-### Confirmation Dialogs
-```python title="todo.py" showLineNumbers{1}
-def delete_all_task():
- confirm = input("Are you sure you want to delete all tasks? (Y/N): ")
- if confirm in ("Y", "y"):
- with open("todo.txt", "w") as f:
- f.write("")
- print("All Tasks Deleted Successfully.")
+def by_priority(priority):
+ return [t for t in read_tasks() if t["priority"] == priority and not t["done"]]
+
+def overdue():
+ today = date.today().isoformat()
+ return [t for t in read_tasks() if t["due"] and t["due"] < today and not t["done"]]
```
-Provides user confirmation for destructive operations to prevent accidental data loss.
+## Migrate to SQLite
-### Application Control
-```python title="todo.py" showLineNumbers{1}
-def exit():
- confirm = input("Are you sure you want to exit? (Y/N): ")
- if confirm in ("Y", "y"):
- print("Exiting...")
- time.sleep(1)
- sys.exit()
+When you cross a thousand tasks, JSON's "rewrite the whole file" model gets slow. SQLite handles thousands trivially:
+```python title="sqlite_store.py"
+import sqlite3, contextlib
+DB = "todo.db"
+with contextlib.closing(sqlite3.connect(DB)) as db:
+ db.execute("""CREATE TABLE IF NOT EXISTS tasks (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT NOT NULL,
+ priority TEXT DEFAULT 'medium',
+ due TEXT,
+ done INTEGER DEFAULT 0,
+ created TEXT DEFAULT CURRENT_TIMESTAMP
+ )""")
+ db.commit()
+```
+Queries become SQL:
+```python title="queries.py"
+db.execute("INSERT INTO tasks(title, priority, due) VALUES(?, ?, ?)",
+ (title, priority, due))
+db.execute("UPDATE tasks SET done = 1 WHERE id = ?", (task_id,))
+rows = db.execute("SELECT * FROM tasks WHERE done = 0 ORDER BY due").fetchall()
```
+Now you have indexes, transactions, and concurrent-read support for free.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| Crash on first run | File does not exist | `if not Path("todo.txt").exists()` |
+| Tasks show with trailing newline | Did not strip | `line.rstrip("\n")` |
+| `IndexError` on delete | Off-by-one | `tasks.pop(n - 1)` and range check |
+| Non-English text shows mojibake | Default encoding | Pass `encoding="utf-8"` everywhere |
+| File grows forever | Never truly cleared | `open("todo.txt", "w").close()` truncates |
+| Two instances corrupt the file | No locking | Use SQLite for multi-process |
+
+## Variations to Try
-Manages application lifecycle with graceful exit confirmation.
-
-### Main Program Loop
-```python title="todo.py" showLineNumbers{1}
-while True:
- choice = input("Enter Choice (1/2/3/4/E/R/H): ")
- if choice == "1":
- add_task()
- elif choice == "2":
- view_task()
- # ... other choices
+### 1. Mark as done (instead of delete)
+Add a `done` flag. Show with strikethrough characters:
+```python title="strike.py"
+title = "".join(c + "̶" for c in task["title"])
```
-Implements continuous operation loop with clear menu options and input validation.
+### 2. Edit a task
+Prompt for a number, then a new title; rewrite in place.
-## Features
+### 3. Tags / categories
+A `tags: list[str]` field. Filter by tag with `t for t in tasks if tag in t["tags"]`.
-- **Persistent Storage**: Tasks saved to file system survive application restarts
-- **CRUD Operations**: Complete Create, Read, Update, Delete functionality
-- **User-Friendly Interface**: Clear menu system with numbered options
-- **Input Validation**: Handles invalid choices and task numbers gracefully
-- **Confirmation Dialogs**: Prevents accidental deletion of tasks
-- **Help System**: Built-in help explaining all available operations
-- **Application Control**: Exit and restart functionality with confirmations
+### 4. Smart input
+`pip install dateparser` lets users type `"finish report next friday"` and parse out a due date.
-## Next Steps
+### 5. Recurring tasks
+A `recurrence` field (`daily`, `weekly`). When marked done, schedule the next occurrence automatically.
+
+### 6. Reminders
+Combine with [Simple Reminder App](./simplereminderapp) — when a task's `due` arrives, fire a desktop notification.
-### Enhancements
-- Add task due dates and reminders
-- Implement task priorities (high, medium, low)
-- Create task categories or tags
-- Add task completion status tracking
-- Implement search and filter functionality
-- Create backup and restore features
-- Add task editing capabilities
-- Include task creation timestamps
-
-### Learning Extensions
-- Study database integration (SQLite)
-- Explore GUI development with Tkinter
-- Learn about task scheduling with datetime
-- Practice with JSON data storage
-- Understand regular expressions for search
-- Explore web-based todo applications
+### 7. Sync across devices
+Save the JSON file to a synced folder (Dropbox/iCloud) or push to a small Flask backend (see [Basic Web Server](./basicwebserver)) so multiple devices see the same list.
+
+### 8. CLI flags
+With **argparse** or **Typer**:
+```bash title="cli"
+todo add "Buy bread" --priority high --due 2025-06-01
+todo list --priority high
+todo done 3
+```
+
+### 9. GUI version
+A Tkinter window with a list of tasks, checkboxes for done, and an entry field. See [Basic Music Player](./basicmusicplayer) for the GUI pattern.
+
+### 10. TUI version
+**`rich`** or **`textual`** for a colorful in-terminal UI with keyboard navigation.
+
+### 11. Export
+"Export to CSV" / "Export to Markdown" — useful for sharing or backup.
+
+### 12. Statistics
+Show "completed this week", "average completion time", "most common priority".
+
+## Best Practices Demonstrated
+- **Confirm destructive operations** before performing them.
+- **Validate input** at the boundary (numeric, in-range, non-empty).
+- **Always use `with open(...)`** for file I/O.
+- **Pass `encoding="utf-8"`** explicitly on every text file.
+- **Move from text → JSON → SQLite** as needs grow; do not skip ahead.
+
+## Real-World Applications
+- Personal task management.
+- Team shared todo (with the Flask variant).
+- Bug-tracker prototypes for small projects.
+- Shopping list / grocery apps.
+- Build-step tracker for repetitive workflows.
## Educational Value
+- **CRUD operations** — the foundation of every data app.
+- **File I/O** — text vs. JSON vs. SQLite trade-offs.
+- **Input validation** — handling the messy reality of humans typing.
+- **Confirmation flows** — UX for irreversible actions.
+- **Refactoring** — going from a 50-line script to a tested, indexed, multi-user app.
+
+## Next Steps
+- Add **mark as done** (without deletion).
+- Switch storage to **JSON**, then **SQLite**.
+- Add **priorities, due dates, tags**.
+- Wrap with a **Tkinter GUI** or a **Flask web app**.
+- Layer **reminders** (cross-link with [Simple Reminder App](./simplereminderapp)).
+- See [Todo List GUI](./todolist) for the next step.
-This project teaches:
-- **File I/O Operations**: Reading from and writing to text files
-- **Data Persistence**: Maintaining application data between sessions
-- **CRUD Operations**: Implementing complete data management functionality
-- **Input Validation**: Handling user input safely and providing feedback
-- **Application Architecture**: Organizing code into focused functions
-- **User Experience**: Creating intuitive command-line interfaces
-- **Error Handling**: Managing edge cases and invalid operations
-- **Control Flow**: Managing complex program flow with menus and confirmations
-
-Perfect for understanding data management, file operations, and user interface design in console applications.
+## Conclusion
+You built a complete todo app and learned the path from a text file to a proper relational schema. Every project you write from here will reuse these CRUD, persistence, and confirmation patterns. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/todo.py). Explore more productivity projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/todolist.mdx b/src/content/docs/projects/Beginners/todolist.mdx
index 3025f1b2..58818ff7 100644
--- a/src/content/docs/projects/Beginners/todolist.mdx
+++ b/src/content/docs/projects/Beginners/todolist.mdx
@@ -1,6 +1,6 @@
---
-title: Todo List Application
-description: A simple Todo List Application using Python. In this project, you can add, delete, and view the list of tasks. This is a command line based application and uses the txt file to store the tasks.
+title: Todo List Application (CLI)
+description: Build a polished CLI todo manager in Python with text-file storage, priorities, due dates, completion tracking, and a path to JSON, SQLite, and Tkinter. Companion to the simpler todo.py walkthrough.
sidebar:
order: 4
hero:
@@ -13,310 +13,296 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-This is a simple Todo List Application using Python. In this project, you can add, delete, and view the list of tasks. This is a command line based application and uses the txt file to store the tasks. For the features, you can add a task, delete a task, and view the list of tasks. The application will store the tasks in the txt file and will read the tasks from the txt file. The application will also check if the txt file exists or not. If the txt file does not exist, then the application will create a new txt file and store the tasks in it. The application will also check if the task is already present in the txt file or not. If the task is already present in the txt file, then the application will not add the task to the txt file. The application will also check if the task is present in the txt file or not. If the task is not present in the txt file, then the application will not delete the task from the txt file.
+The Todo List Application is the canonical first useful program in any language. It exercises file I/O, persistence, CRUD operations, input validation, menu design, and confirmation flows in under 150 lines. In this expanded tutorial you build the polished command-line version with **Add / View / Delete / Delete-all / Exit / Restart / Help** commands stored in a plain `todo.txt`, then evolve toward a real productivity tool: priorities, due dates, completion marks, JSON storage, SQLite, and a Tkinter GUI.
+
+You will learn:
+- Robust file I/O with `with open` and UTF-8 encoding.
+- The full CRUD cycle on a flat text file.
+- Why confirmation flows matter for destructive operations.
+- The architectural path from `todo.txt` → JSON → SQLite.
+- How to grow a 30-line script into a tool you would actually use.
## Prerequisites
-- Python 3.6 or above
-- Text Editor or IDE
+- Python 3.6 or above.
+- A text editor or IDE.
+- Comfort with `input`, `if/elif/else`, and `while`.
+- Familiarity with the lighter [Todo (basics)](./todo) walkthrough.
## Getting Started
-### Creating a new project
-1. Create a new project folder and name it as `todo`.
-2. Create a new file and name it as `todo.py`.
-3. Create a new file and name it as `todo.txt`.
-4. Open the project folder in the text editor or IDE.
-5. Copy the code from `todo.py` and paste it into `todo.py` file.
+
+### Create the project
+1. Create folder `todo-list`.
+2. Inside, create `todo.py`.
+3. The script will create `todo.txt` on first save.
### Write the code
-1. Add the following code to the `todo.py` file.
-2. Save the File.
-3. Open the terminal and navigate to the project folder.
-4. Run the following command to run the application.
-```cmd title="command" showLineNumbers{1}
+
+### Run it
+```cmd title="command"
C:\Users\username\Documents\todo> python todo.py
Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H): 1
-Add Task
----------
-Enter Task: Going for the google summit
+1. Add task 2. View task 3. Delete task 4. Delete all
+E. Exit R. Restart H. Help
+Enter Choice: 1
+Enter Task: Going for the Google summit
Task Added Successfully.
-Enter Choice (1/2/3/4/E/R/H): 2
-View Task
----------
-1. Going for the google summit
-Enter Choice (1/2/3/4/E/R/H): 3
-Delete Task
-------------
-1. Going for the google summit
-Enter Task Number to Delete: 1
-Task Deleted Successfully.
-Enter Choice (1/2/3/4/E/R/H): 2
-View Task
----------
-No Tasks Found.
-Enter Choice (1/2/3/4/E/R/H): H
-Help
-----
-Add Task: Add a task to the todo list.
-View Task: View all tasks in the todo list.
-Delete Task: Delete a task from the todo list.
-Delete All Task: Delete all tasks from the todo list.
-Exit: Exit the application.
-Restart: Restart the application.
-Help: View help.
-Enter Choice (1/2/3/4/E/R/H): E
-Exit
------
-Are you sure you want to exit? (Y/N): Y
-Exiting...
```
-5. You can view the tasks in the `todo.txt` file.
-```txt title="todo.txt" showLineNumbers{1}
-Going for the google summit
+
+## Step-by-Step Explanation
+
+### 1. Imports
+```python title="todo.py"
+import os, sys, time
```
+- `os` for the restart trick.
+- `sys` for `sys.exit()`.
+- `time` for the sleep before restart.
-## Explanation
-1. The `import os` statement will import the `os` module.
-2. The `import time` statement will import the `time` module.
-3. The `import sys` statement will import the `sys` module.
-4. The `import datetime` statement will import the `datetime` module.
-5. The `def add_task():` statement will define a function named `add_task`. It will add a task to the todo list. It will take no parameters.
-6. `add_task()` function write the task to the `todo.txt` file using the `with open("todo.txt", "a") as f:` statement. It append the task at the end of the file.
-7. The `def view_task():` statement will define a function named `view_task`. It will view all tasks in the todo list. It will take no parameters.
-8. `view_task()` function read the tasks from the `todo.txt` file using the `with open("todo.txt", "r") as f:` statement.
-9. The `def delete_task():` statement will define a function named `delete_task`. It will delete a task from the todo list. It will take no parameters.
-10. `delete_task()` function read the tasks from the `todo.txt` file using the `with open("todo.txt", "r") as f:` statement. It will ask the user to enter the task number to delete. It will delete the task from the `todo.txt` file using the `del tasks[task_no - 1]` statement.
-11. The `def delete_all_task():` statement will define a function named `delete_all_task`. It will delete all tasks from the todo list. It will take no parameters.
-12. `delete_all_task()` function read the tasks from the `todo.txt` file using the `with open("todo.txt", "r") as f:` statement. It will ask the user to confirm the deletion of all tasks. It will delete all tasks from the `todo.txt` file using the `with open("todo.txt", "w") as f:` statement.
-13. The `def exit():` statement will define a function named `exit`. It will exit the application. It will take no parameters. It will ask the user to confirm the exit. It will exit the application using the `sys.exit()` statement.
-14. The `def restart():` statement will define a function named `restart`. It will restart the application. It will take no parameters. It will ask the user to confirm the restart. It will restart the application using the `os.system("python todo.py")` statement.
-15. The `def help():` statement will define a function named `help`. It will display the help. It will take no parameters. It will display the help using the `print()` statement. It helps the user to use the application.
-16. The `print("Todo Application")` statement will display the Todo Application text on the screen.
-17. We are going to use `while` loop to run the application continuously. The `while True:` statement will create an infinite loop.
-18. When the user wants to exit the application, then the `sys.exit()` statement will exit the application.
-
-## Usage
-1. Open the terminal and navigate to the project folder.
-2. Run the following command to run the application.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\hello_world> python todo.py
-Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H):
+### 2. Add a task
+```python title="todo.py"
+def add_task():
+ task = input("Enter task: ").strip()
+ if not task:
+ print("Empty task ignored.")
+ return
+ with open("todo.txt", "a", encoding="utf-8") as f:
+ f.write(task + "\n")
+ print("Task added.")
```
-3. Enter the choice and press the enter key.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\hello_world> python todo.py
-Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H): 1
+- `"a"` opens for append — does not erase existing content.
+- `.strip()` removes trailing whitespace including the newline from Enter.
+- Empty task guard avoids accidental blank entries.
+
+### 3. View tasks
+```python title="todo.py"
+def view_tasks():
+ if not os.path.exists("todo.txt") or os.path.getsize("todo.txt") == 0:
+ print("No tasks yet.")
+ return
+ with open("todo.txt", encoding="utf-8") as f:
+ tasks = [line.rstrip("\n") for line in f if line.strip()]
+ for i, t in enumerate(tasks, start=1):
+ print(f"{i}. {t}")
```
+- `enumerate(..., start=1)` gives 1-based numbering for the user.
+- The filter `if line.strip()` skips blank lines that may sneak in.
-4. If you enter `1` and press the enter key, then the application will add a task to the todo list.
-```cmd title="command" showLineNumbers{1}
-Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H): 1
-Add Task
----------
-Enter Task: Going for the google summit
-Task Added Successfully.
-Enter Choice (1/2/3/4/E/R/H):
+### 4. Delete by number
+```python title="todo.py"
+def delete_task():
+ if not os.path.exists("todo.txt"):
+ print("Nothing to delete."); return
+ with open("todo.txt", encoding="utf-8") as f:
+ tasks = [line.rstrip("\n") for line in f if line.strip()]
+ if not tasks:
+ print("Nothing to delete."); return
+ for i, t in enumerate(tasks, 1): print(f"{i}. {t}")
+ try:
+ n = int(input("Number to delete: "))
+ except ValueError:
+ print("Enter a whole number."); return
+ if not 1 <= n <= len(tasks):
+ print("Out of range."); return
+ removed = tasks.pop(n - 1)
+ with open("todo.txt", "w", encoding="utf-8") as f:
+ f.writelines(t + "\n" for t in tasks)
+ print(f"Deleted: {removed}")
```
+The "load all, mutate, write all back" pattern is fine for small files (thousands of lines). Beyond that, SQLite is the right answer.
-5. If you enter `2` and press the enter key, then the application will view all tasks in the todo list.
-```cmd title="command" showLineNumbers{1}
-Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H): 2
-View Task
----------
-1. Going for the google summit
-Enter Choice (1/2/3/4/E/R/H):
+### 5. Delete all (with confirmation)
+```python title="todo.py"
+def delete_all():
+ if input("Delete EVERY task? (Y/N): ").strip().lower() != "y":
+ print("Cancelled."); return
+ open("todo.txt", "w").close()
+ print("All tasks deleted.")
```
+Confirmation flows are not optional — accidental "delete all" is the #1 way users lose data.
-6. If you enter `3` and press the enter key, then the application will delete a task from the todo list. Application shows the list of tasks and asks the user to enter the task number to delete.
-```cmd title="command" showLineNumbers{1}
-Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H): 3
-Delete Task
-------------
-1. Going for the google summit
-Enter Task Number to Delete: 1
-Task Deleted Successfully.
-Enter Choice (1/2/3/4/E/R/H):
+### 6. Exit, Restart, Help
+```python title="todo.py"
+def exit_app():
+ if input("Exit? (Y/N): ").strip().lower() == "y":
+ print("Goodbye.")
+ sys.exit()
+
+def restart():
+ if input("Restart? (Y/N): ").strip().lower() == "y":
+ print("Restarting..."); time.sleep(1)
+ os.execv(sys.executable, [sys.executable] + sys.argv)
+
+def show_help():
+ print("Add / View / Delete / Delete All / Exit / Restart / Help")
```
+- **`sys.exit()`** is the clean way to quit.
+- **`os.execv`** replaces the current process with a fresh interpreter — true restart with no state.
-7. If you enter `4` and press the enter key, then the application will delete all tasks from the todo list. Application shows the list of tasks and asks the user to confirm the deletion of all tasks.
-```cmd title="command" showLineNumbers{1}
-Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H): 4
-Delete All Task
-----------------
-1. Going for the google summit
-Are you sure you want to delete all tasks? (Y/N): Y
-All Tasks Deleted Successfully.
-Enter Choice (1/2/3/4/E/R/H):
+### 7. The main loop
+```python title="todo.py"
+while True:
+ choice = input("Choice (1/2/3/4/E/R/H): ").strip().upper()
+ if choice == "1": add_task()
+ elif choice == "2": view_tasks()
+ elif choice == "3": delete_task()
+ elif choice == "4": delete_all()
+ elif choice == "E": exit_app()
+ elif choice == "R": restart()
+ elif choice == "H": show_help()
+ else: print("Invalid choice.")
```
+`.strip().upper()` makes `e`, `E`, `e `, ` E ` all equivalent.
-8. If you enter `E` or `e` and press the enter key, then the application will exit.
-```cmd title="command" showLineNumbers{1}
-Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H): E
-Exit
------
-Are you sure you want to exit? (Y/N): Y
-Exiting...
+## Architecture: From txt → JSON → SQLite
+
+A `todo.txt` is great for ≤ ~500 tasks. Pain points beyond that:
+- No structured fields — priority, due date, tags, status.
+- Editing a single task means rewriting the whole file.
+- Concurrent access from two terminals corrupts it.
+
+### Step 1: JSON
+```python title="json_store.py"
+import json
+from pathlib import Path
+
+STORE = Path("todo.json")
+def read():
+ return json.loads(STORE.read_text(encoding="utf-8")) if STORE.exists() else []
+def write(tasks):
+ STORE.write_text(json.dumps(tasks, indent=2), encoding="utf-8")
+
+def add(title, priority="medium", due=None):
+ tasks = read()
+ tasks.append({
+ "id": len(tasks) + 1,
+ "title": title, "priority": priority, "due": due, "done": False,
+ })
+ write(tasks)
```
-9. If you enter `R` or `r` and press the enter key, then the application will restart.
-```cmd title="command" showLineNumbers{1}
-Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H): R
-Restart
---------
-Are you sure you want to restart? (Y/N): Y
-Restarting...
+### Step 2: SQLite
+```python title="sqlite_store.py"
+import sqlite3
+db = sqlite3.connect("todo.db")
+db.execute("""CREATE TABLE IF NOT EXISTS tasks (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT NOT NULL,
+ priority TEXT DEFAULT 'medium',
+ due TEXT,
+ done INTEGER DEFAULT 0,
+ created TEXT DEFAULT CURRENT_TIMESTAMP
+)""")
+db.execute("INSERT INTO tasks(title, priority) VALUES(?, ?)", ("Buy bread", "high"))
+db.commit()
+
+# query
+for row in db.execute("SELECT id, title FROM tasks WHERE done = 0 ORDER BY due"):
+ print(row)
```
+Now you have indexes, transactions, concurrent reads, and SQL queries — all from one Python module.
-10. If you enter `H` or `h` and press the enter key, then the application will display the help.
-```cmd title="command" showLineNumbers{1}
-Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H): H
-Help
-----
-Add Task: Add a task to the todo list.
-View Task: View all tasks in the todo list.
-Delete Task: Delete a task from the todo list.
-Delete All Task: Delete all tasks from the todo list.
-Exit: Exit the application.
-Restart: Restart the application.
-Help: View help.
-Enter Choice (1/2/3/4/E/R/H):
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `FileNotFoundError` on first run | File created lazily | Guard with `os.path.exists` |
+| Tasks shown with trailing newline | Forgot to strip | `line.rstrip("\n")` |
+| `IndexError` on delete | Off-by-one | `tasks.pop(n - 1)` with range check |
+| Mojibake on non-English tasks | Default encoding | Always `encoding="utf-8"` |
+| Empty task added on Enter-only | No empty check | `if not task: return` |
+| Two terminals corrupt the file | No locking | Move to SQLite for concurrent access |
+| Restart leaves stale state | Manual reset of globals incomplete | Use `os.execv` for clean restart |
+
+## Variations to Try
+
+### 1. Mark done (instead of delete)
+Add a `done` flag. Show `[x]` or `[ ]` prefix. Filter with `view --pending`.
+
+### 2. Priorities
+`high`, `medium`, `low` — sort by priority on view.
+
+### 3. Due dates
+`due: YYYY-MM-DD`. Highlight overdue in red on output.
+```python title="overdue.py"
+from datetime import date
+RED, RESET = "\033[91m", "\033[0m"
+overdue = task["due"] and task["due"] < date.today().isoformat()
+print(f"{RED if overdue else ''}{task['title']}{RESET if overdue else ''}")
```
-11. If you enter any other choice and press the enter key, then the application will display the invalid choice message.
-```cmd title="command" showLineNumbers{1}
-Todo Application
-----------------
-Select Operation.
-1. Add Task
-2. View Task
-3. Delete Task
-4. Delete All Task
-E. Exit
-R. Restart
-H. Help
-Enter Choice (1/2/3/4/E/R/H): 5
-Invalid Choice.
-Enter Choice (1/2/3/4/E/R/H):
+### 4. Tags
+A `tags: list[str]` field. Filter with `view --tag work`.
+
+### 5. Search
+`view --search keyword`.
+
+### 6. Edit
+"Edit task #3" → prompt for new title.
+
+### 7. Recurring tasks
+`recurrence: daily | weekly | monthly`. On complete, schedule the next occurrence.
+
+### 8. Reminders
+Combine with [Simple Reminder App](./simplereminderapp) — fire a desktop notification when `due` arrives.
+
+### 9. CLI flags with `argparse` or `typer`
+```bash title="cli"
+todo add "Buy bread" --priority high --due 2026-06-01
+todo list --pending --tag groceries
+todo done 3
+todo delete 5
```
-## Next Steps
-Congratulations you have successfully created a Todo List Application using Python. Experiment with the code and try to modify it.
-
-Here are a few suggestions:
-1. Add a feature to edit a task.
-2. Add a feature to sort the tasks.
-3. Add a feature to search for a task.
-4. Add a feature to add a task to a specific position.
-5. Add a feature to add a task to a specific date.
-6. Add a feature to add a task to a specific time.
-7. Add a feature to add a task to a specific date and time.
-8. Add a feature to add a task to a specific date and time and set a reminder.
-9. Add a feature to add a task to a specific date and time and set a reminder and repeat the task.
-10. Add a feature to add a task to a specific date and time and set a reminder and repeat the task and set a priority.
+### 10. GUI version
+Tkinter window with a `Listbox` of tasks, checkboxes for done, and an entry field for new tasks.
-## Conclusion
-In this project, You learned how to build a Todo List Application using Python. You also learned how to add a task, view a task, delete a task, and delete all tasks.
+### 11. Web frontend
+Flask app (see [Basic Web Server](./basicwebserver)) with HTMX or React, JSON API in the middle.
-You also learned how to use the `os` module, `time` module, `sys` module, and `datetime` module. You also learned how to use the `with` statement. You also learned how to use the `while` loop. You also learned how to use the `if` statement. You also learned how to use the `elif` statement. You also learned how to use the `else` statement. You also learned how to use the `def` statement. You also learned how to use the `print()` function. You also learned how to use the `input()` function. You also learned how to use the `open()` function. You also learned how to use the `del` statement. You also learned how to use the `sys.exit()` function. You can find more information about the `os` module, `time` module, `sys` module, and `datetime` module on the official [Python documentation website](https://docs.python.org/3/library/).
+### 12. Sync across devices
+Write the JSON to a shared cloud folder (Dropbox / iCloud) or push to a server. Eventual-consistency conflict resolution.
-Keep experimenting with the code and try to modify it. Find more tutorials on the Python Central Hub.
\ No newline at end of file
+### 13. Pomodoro integration
+"Start working on task #3" → 25-minute timer fires, breaks logged.
+
+### 14. Statistics
+Tasks completed this week / month. Streaks, longest-pending task, average time-to-done.
+
+### 15. Export
+`todo export markdown > tasks.md` → renders nicely on GitHub.
+
+## Companion Tutorial
+
+This page covers the **more detailed CLI walkthrough**. For the *introduction* with a simpler scope, see [Todo (basics)](./todo) — same script, simpler narrative. For the **GUI version**, see the upcoming "Todo Tkinter" project.
+
+## Best Practices Demonstrated
+- **Always `with open(...)`** for file I/O.
+- **`encoding="utf-8"` always** to avoid silent corruption.
+- **Confirm destructive operations.**
+- **Validate input** at the moment it enters.
+- **Migrate storage as you grow** — txt → JSON → SQLite, never the reverse.
+
+## Real-World Applications
+- Personal task management.
+- Project to-do lists.
+- Quick reminders before they become reminders.
+- Shopping / grocery lists.
+- Bug tracker prototypes for solo projects.
+
+## Educational Value
+- **CRUD operations** on a flat text file.
+- **Defensive UX** — confirmations, validations, friendly errors.
+- **Storage trade-offs** — text vs. JSON vs. SQLite.
+- **Process control** — `sys.exit`, `os.execv`, `time.sleep`.
+
+## Next Steps
+- Add **mark-as-done** (don't delete).
+- Migrate to **JSON storage**.
+- Add **priorities and due dates**.
+- Wrap with a **Tkinter GUI**.
+- Layer **reminders** via [Simple Reminder App](./simplereminderapp).
+- Cross-link with [Todo (basics)](./todo) for the simpler intro.
+
+## Conclusion
+You built a complete CLI todo manager with persistence, CRUD, and confirmation flows — the same architecture used by every text-file-backed productivity tool from `taskwarrior` to early WordPress drafts. Every step beyond (priorities, due dates, GUIs, sync) reuses these primitives. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/todo.py). Explore more productivity projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/urlshorter.mdx b/src/content/docs/projects/Beginners/urlshorter.mdx
index 84620edc..d390f88f 100644
--- a/src/content/docs/projects/Beginners/urlshorter.mdx
+++ b/src/content/docs/projects/Beginners/urlshorter.mdx
@@ -1,6 +1,6 @@
---
title: URL Shortener
-description: A simple URL shortener application using Python that converts long URLs into short, shareable links using the pyshorteners library.
+description: Build a Python URL shortener using pyshorteners (TinyURL, Bitly) and then build your own from scratch with Flask + base62 encoding. Learn third-party APIs, error handling, and how short links actually work.
sidebar:
order: 15
hero:
@@ -13,149 +13,272 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-URL Shortener is a simple Python application that converts long URLs into short, manageable links using the `pyshorteners` library. The application supports multiple URL shortening services including TinyURL and Bitly. This project demonstrates working with external APIs, URL manipulation, and third-party libraries. Users can input a long URL and receive a shortened version that redirects to the original link, making it perfect for sharing on social media or in constrained spaces.
+A URL shortener compresses a long URL like `https://example.com/very/long/path?query=1&foo=2` into something like `https://tinyurl.com/abc123`. The short link redirects to the original when clicked. In this tutorial you will start by using the **`pyshorteners`** library to talk to existing services (TinyURL, Bitly), then go deeper and build **your own** shortener with Flask, SQLite, and a base62 encoder — the same architecture every commercial shortener uses.
-## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
-- pyshorteners module
-- Internet connection
+You will learn:
+- How third-party APIs wrap real services.
+- How HTTP redirects work.
+- How short codes are generated and looked up.
+- Why custom shorteners need careful URL validation.
+- How to add analytics (click counts, referrers) and rate limiting.
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download). You must have the pyshorteners module installed on your computer. You can install it using the following command:
+## Prerequisites
+- Python 3.6 or above.
+- A code editor or IDE.
+- An internet connection (for the third-party portion).
+- Comfort with `pip install` and basic web concepts.
-```cmd title="command" showLineNumbers{1} {1}
-C:\Users\Your Name>pip install pyshorteners
+## Install Dependencies
+```cmd title="install"
+pip install pyshorteners
+```
+For the "build your own" section later:
+```cmd title="install"
+pip install flask
```
-For Bitly integration (optional), you'll need to:
-1. Create a free account at [Bitly](https://bitly.com/)
-2. Get your API key from the Bitly developer dashboard
-3. Replace the placeholder API key in the code
+## Part 1 — Use a Third-Party Service
-## Getting Started
-#### Create a Project
+### Getting started
1. Create a folder named `url-shortener`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `urlshorter.py`.
-4. Copy the given code and paste it in your `urlshorter.py` file.
+2. Inside it, create `urlshorter.py`.
-## Write the Code
-1. Copy and paste the following code in your `urlshorter.py` file.
+### Write the code
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `url-shortener`.
+
+### Run it
```cmd title="command" showLineNumbers{1} {1-10}
C:\Users\Your Name\url-shortener> python urlshorter.py
Enter URL: https://www.example.com/very/long/url/with/many/parameters?param1=value1¶m2=value2
-Short URL: https://tinyurl.com/abc123def
+Short URL: https://tinyurl.com/abc123def
+```
-C:\Users\Your Name\url-shortener> python urlshorter.py
-Enter URL: https://github.com/Ravikisha/PythonCentralHub
-Short URL: https://tinyurl.com/pythonhub123
+## Step-by-Step Explanation
+
+### 1. Import the library
+```python title="urlshorter.py"
+import pyshorteners
```
-## Explanation
-1. Import the required module.
-```python title="urlshorter.py" showLineNumbers{1} {1}
-import pyshorteners # pip install pyshorteners
+### 2. Validate and shorten
+```python title="urlshorter.py"
+url = input("Enter URL: ").strip()
+if not (url.startswith("http://") or url.startswith("https://")):
+ raise SystemExit("URL must start with http:// or https://")
+
+shortener = pyshorteners.Shortener()
+short_url = shortener.tinyurl.short(url)
+print("Short URL:", short_url)
```
+- TinyURL needs no API key.
+- Bitly does — set up an API token and use `pyshorteners.Shortener(api_key="...")`.
-2. Get the URL input from the user.
-```python title="urlshorter.py" showLineNumbers{1} {1}
-url = input("Enter URL: ")
+### 3. Handle network errors
+```python title="urlshorter.py"
+from pyshorteners.exceptions import ShorteningErrorException, ExpandingErrorException
+import requests
+
+try:
+ short_url = shortener.tinyurl.short(url)
+except (ShorteningErrorException, requests.RequestException) as e:
+ print("Could not shorten:", e)
```
-3. Create a shortener object and generate short URL using TinyURL.
-```python title="urlshorter.py" showLineNumbers{1} {1-3}
-shortener = pyshorteners.Shortener()
-shortURL = shortener.tinyurl.short(url)
+## What `pyshorteners` Supports
+
+| Service | Needs API key? |
+|---|---|
+| TinyURL | No |
+| Is.gd | No |
+| V.gd | No |
+| Clck.ru | No |
+| Chilp.it | No |
+| Bitly | Yes |
+| QR.net | No |
+| Owly | Yes |
+| Cuttly | Yes |
+
+Switching service is just changing the attribute: `shortener.isgd.short(url)`, `shortener.bitly.short(url)`, etc.
+
+## Part 2 — Build Your Own Shortener
+
+Using a service is fine. Building one yourself is far more educational and gives you full control.
+
+### How a real shortener works
+
+1. User submits a long URL.
+2. Server stores `{ short_code → long_url }` in a database.
+3. Server returns `https://short.example.com/`.
+4. When that short URL is visited, the server looks up the code and returns an **HTTP 302 redirect** to the long URL.
+
+The short code is usually a base62 (`0-9a-zA-Z`) encoding of an auto-incrementing ID — 6 characters yields ~57 billion possible URLs.
+
+### Code
+```python title="own_shortener.py"
+from flask import Flask, request, redirect, jsonify
+import sqlite3, string, secrets
+
+app = Flask(__name__)
+ALPHABET = string.ascii_letters + string.digits # 62 characters
+
+def init_db():
+ with sqlite3.connect("urls.db") as db:
+ db.execute("""CREATE TABLE IF NOT EXISTS urls (
+ code TEXT PRIMARY KEY,
+ url TEXT NOT NULL,
+ clicks INTEGER DEFAULT 0
+ )""")
+
+def new_code(length=6):
+ return "".join(secrets.choice(ALPHABET) for _ in range(length))
+
+@app.post("/shorten")
+def shorten():
+ long_url = request.json["url"].strip()
+ if not long_url.startswith(("http://", "https://")):
+ return jsonify(error="URL must include http:// or https://"), 400
+ with sqlite3.connect("urls.db") as db:
+ # generate codes until one is unique
+ while True:
+ code = new_code()
+ try:
+ db.execute("INSERT INTO urls(code, url) VALUES(?, ?)",
+ (code, long_url))
+ break
+ except sqlite3.IntegrityError:
+ continue
+ return jsonify(short=f"{request.host_url}{code}")
+
+@app.get("/")
+def follow(code):
+ with sqlite3.connect("urls.db") as db:
+ row = db.execute("SELECT url FROM urls WHERE code=?", (code,)).fetchone()
+ if not row:
+ return "Not found", 404
+ db.execute("UPDATE urls SET clicks = clicks + 1 WHERE code=?", (code,))
+ return redirect(row[0], code=302)
+
+if __name__ == "__main__":
+ init_db()
+ app.run(debug=True)
+```
+
+Test it:
+```bash title="test"
+curl -X POST http://localhost:5000/shorten -H "Content-Type: application/json" \
+ -d '{"url":"https://python.org"}'
+# {"short":"http://localhost:5000/aB3xY9"}
+
+curl -I http://localhost:5000/aB3xY9
+# HTTP/1.1 302 FOUND
+# Location: https://python.org
```
-4. Alternative Bitly implementation (commented out).
-```python title="urlshorter.py" showLineNumbers{1} {1-3}
-# bitlyShortener = pyshorteners.Shortener(api_key='your_api_key_here')
-# shortURL = bitlyShortener.bitly.short(url)
+### Why base62
+
+- **62 chars** (`0–9 a–z A–Z`) is URL-safe and case-sensitive.
+- 6 characters = `62 ** 6 ≈ 57 billion` possible short URLs.
+- 8 characters = `218 trillion`. Plenty.
+
+Some shorteners use a **counter** (auto-increment ID) encoded as base62, which guarantees uniqueness without retries.
+
+## Validating URLs Properly
+
+User input is a frequent vector for **open-redirect** attacks. Validate:
+
+```python title="validate.py"
+from urllib.parse import urlparse
+
+def is_valid_url(u: str) -> bool:
+ try:
+ p = urlparse(u)
+ return p.scheme in {"http", "https"} and bool(p.netloc) and "." in p.netloc
+ except Exception:
+ return False
```
+Reject `javascript:`, `data:`, `file:` schemes — they would let an attacker craft phishing-style short links.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `pyshorteners.exceptions.ShorteningErrorException` | Provider down, malformed URL, rate-limited | Catch and retry/back off |
+| Same short code generated for two URLs | Did not check uniqueness | `INSERT` with `PRIMARY KEY` on code → catch `IntegrityError` |
+| Redirect goes nowhere | Returned a 200 with the URL in the body | Return `redirect(url, code=302)` |
+| Open redirect to `javascript:` | No URL validation | Whitelist `http/https` schemes only |
+| Database lock errors | SQLite with concurrent writers | Use a real DB (Postgres) for multi-process deployments |
+
+## Variations to Try
-5. Display the shortened URL.
-```python title="urlshorter.py" showLineNumbers{1} {1}
-print("Short URL: ", shortURL)
+### 1. Custom aliases
+Let the user request `/mylink` instead of a random code:
+```python title="alias.py"
+alias = request.json.get("alias")
+if alias and not db.execute("SELECT 1 FROM urls WHERE code=?", (alias,)).fetchone():
+ code = alias
```
-## Supported Services
-This URL shortener supports multiple services:
-
-### TinyURL (Default)
-- **Pros:** No API key required, simple to use
-- **Cons:** Limited customization options
-- **Usage:** Included in the default code
-
-### Bitly
-- **Pros:** Custom short URLs, analytics, branded links
-- **Cons:** Requires API key registration
-- **Usage:** Uncomment the Bitly section and add your API key
-
-### Other Supported Services
-The `pyshorteners` library also supports:
-- Is.gd
-- V.gd
-- QR.net
-- Chilp.it
-- Clck.ru
-- And many more!
-
-## Features
-- Convert long URLs to short, shareable links
-- Support for multiple URL shortening services
-- Simple command-line interface
-- No database required
-- Instant URL conversion
-- Works with any valid HTTP/HTTPS URL
-- Cross-platform compatibility
-
-## How to Use
-1. Run the application
-2. Enter the long URL you want to shorten
-3. Press Enter to get the shortened URL
-4. Copy and share the short URL
-5. The short URL will redirect to the original long URL when clicked
-
-## API Key Setup (For Bitly)
-To use Bitly services:
-1. Visit [Bitly Developer](https://dev.bitly.com/)
-2. Create a free account
-3. Generate an API access token
-4. Replace `'your_api_key_here'` with your actual API key
-5. Uncomment the Bitly code section
+### 2. Expiration dates
+Add `expires_at TIMESTAMP NULL`. On follow, return 410 Gone if expired.
-## Next Steps
-You can enhance this project by:
-- Adding a GUI interface using Tkinter
-- Creating a web application using Flask
-- Implementing URL validation
-- Adding custom short URL aliases
-- Creating a URL history database
-- Adding QR code generation for short URLs
-- Implementing click analytics
-- Adding bulk URL shortening
-- Creating browser extension integration
-
-## Error Handling
-Consider adding error handling for:
-- Invalid URLs
-- Network connectivity issues
-- API rate limits
-- Service unavailability
-
-Example enhanced version:
-```python title="urlshorter.py" showLineNumbers{1}
-try:
- shortURL = shortener.tinyurl.short(url)
- print("Short URL: ", shortURL)
-except Exception as e:
- print("Error creating short URL:", str(e))
+### 3. Click analytics
+You already store `clicks`. Add a `/stats/` endpoint that returns total clicks plus, optionally, referrer breakdown.
+
+### 4. QR code
+```python title="qr.py"
+import qrcode
+img = qrcode.make(short_url)
+img.save("link.png")
```
+### 5. Password-protected links
+Add a `password_hash` column. Before redirecting, require the user to enter a password.
+
+### 6. Bulk shortening
+Accept a list of URLs and return short versions in one call.
+
+### 7. Rate limiting
+Use `flask-limiter` to cap to e.g. 10 requests/minute per IP.
+
+### 8. Domain restrictions
+Allow shortening only for specific domains — useful for company-internal use.
+
+### 9. CLI
+Wrap the API client with `argparse`:
+```bash title="cli"
+python shorten.py https://example.com --alias homepage
+```
+
+### 10. Deployment
+Run behind **gunicorn** + **nginx**, point a domain like `short.example.com` at it, and you have your own production shortener for ~$5/month.
+
+## Privacy and Security Notes
+
+- **Short URLs are public.** Anyone who guesses the code can follow the link. Do not embed sensitive paths.
+- **Phishing risk.** Short links hide their destination — bad actors abuse this. Consider a preview page (`/p/`) that shows the destination before redirecting.
+- **Log retention.** Server access logs reveal who clicked what. Decide your retention policy.
+- **Open-redirect** is the classic shortener vulnerability — validate schemes carefully.
+
+## Real-World Applications
+- Social-media sharing (Twitter character limits used to drive this).
+- Email and SMS marketing — short, trackable URLs.
+- QR codes that point to a short URL you can rebind later.
+- Branded short links (e.g., `nyti.ms` for the New York Times).
+- Internal tooling — share links to dashboards without copying 200-character URLs.
+
+## Educational Value
+- **Third-party APIs** — calling, error handling, swap-ability.
+- **HTTP redirects** — status codes and what they mean.
+- **Encoding schemes** — base62 versus base64 versus hex.
+- **Database integrity** — unique constraints and how to handle collisions.
+- **Web security basics** — open redirect, input validation, abuse vectors.
+
+## Next Steps
+- Build the **Flask version** above; deploy it somewhere free (Render, Fly.io).
+- Add **analytics** with referrers, country (via MaxMind GeoIP), and time series.
+- Add a **web frontend** with a form and a copy-to-clipboard button.
+- Add **authentication** so each user only sees their own links.
+- Layer on **rate limiting** and **captcha** before opening to the public.
+
## Conclusion
-In this project, we learned how to create a URL Shortener using Python and the `pyshorteners` library. We explored working with external APIs, URL manipulation, and third-party service integration. This project demonstrates how to leverage existing services to create useful applications quickly and efficiently. The skills learned here can be applied to other API-based projects and web service integrations. To find more projects like this, you can visit Python Central Hub.
+You used a third-party shortener and learned how the *actual* underlying mechanism works — a database lookup and an HTTP 302. Building your own version is one of the most concise demonstrations of how the modern web composes simple parts into useful services. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/beginners/urlshorter.py). Explore more web projects on Python Central Hub.
diff --git a/src/content/docs/projects/Beginners/webpagecontentdownloader.mdx b/src/content/docs/projects/Beginners/webpagecontentdownloader.mdx
index 91a21ca0..14626d6d 100644
--- a/src/content/docs/projects/Beginners/webpagecontentdownloader.mdx
+++ b/src/content/docs/projects/Beginners/webpagecontentdownloader.mdx
@@ -1,6 +1,6 @@
---
title: Web Page Content Downloader
-description: A simple Python application that downloads and saves web page content to a local file using urllib library for web scraping and content extraction.
+description: Build a Python tool that downloads web page content, handles encoding, redirects, retries, large files, and produces clean local mirrors. Learn urllib vs requests, streaming, and ethical archiving.
sidebar:
order: 11
hero:
@@ -13,218 +13,235 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
-Web Page Content Downloader is a simple Python application that fetches and saves the HTML content of web pages to local files. Using Python's built-in `urllib` library, this program demonstrates basic web scraping concepts, HTTP requests, and file handling. Users can input any URL and specify a filename to save the web page's source code locally. This project is an excellent introduction to web data extraction and provides the foundation for more advanced web scraping applications.
+Saving a web page to disk is the simplest "real" web project: one HTTP request, one file write. But the gap between "works on `example.com`" and "works on any URL" hides every web-programming concept in one place — encoding, redirects, retries, timeouts, errors, streaming large files, and politeness toward servers. In this tutorial you build the trivial 3-line version with `urllib`, then evolve to a robust downloader using `requests`, streaming for large responses, retry policies, progress bars, and a mirror mode that downloads HTML plus its referenced assets.
+
+You will leave understanding:
+- The HTTP request/response cycle.
+- Why default `urllib` is fine for toys and `requests` is the standard for real code.
+- How content encoding (`gzip`, `br`), character encoding, and redirects all bite naïve scripts.
+- How to stream large responses without loading them into memory.
+- The ethics and legality of downloading — robots.txt, rate limits, copyright.
## Prerequisites
-- Python 3.6 or above
-- A code editor or IDE
-- Internet connection
-
-## Before you Start
-Before starting this project, you must have Python installed on your computer. If you don't have Python installed, you can download it from [here](https://www.python.org/downloads/). You must have a code editor or IDE installed on your computer. If you don't have any code editor or IDE installed, you can download Visual Studio Code from [here](https://code.visualstudio.com/download).
-
-Note: This project uses only built-in Python modules (`urllib`), so no additional installations are required.
-
-## Getting Started
-#### Create a Project
-1. Create a folder named `web-content-downloader`.
-2. Open the folder in your favorite code editor or IDE.
-3. Create a file named `webpagecontentdownloader.py`.
-4. Copy the given code and paste it in your `webpagecontentdownloader.py` file.
-
-## Write the Code
-1. Copy and paste the following code in your `webpagecontentdownloader.py` file.
-
-2. Save the file.
-3. Open the terminal in your code editor or IDE and navigate to the folder `web-content-downloader`.
-```cmd title="command" showLineNumbers{1} {1-10}
+- Python 3.6 or above.
+- A text editor or IDE.
+- Internet connection.
+- Comfort with file I/O.
+
+## Part 1 — The Trivial Version
+
+`urllib` ships with Python so no install is needed.
+
+### Create the project
+1. Create folder `web-content-downloader`.
+2. Inside, create `webpagecontentdownloader.py`.
+
+### Write the code
+
+
+### Run it
+```cmd title="command"
C:\Users\Your Name\web-content-downloader> python webpagecontentdownloader.py
Enter the URL: https://www.example.com
Enter the file name: example.html
-# The program will download the web page content and save it to example.html
-
-C:\Users\Your Name\web-content-downloader> python webpagecontentdownloader.py
-Enter the URL: https://www.python.org
-Enter the file name: python_homepage.html
-# The content of python.org will be saved to python_homepage.html
+# Page content saved to example.html
```
-## Explanation
+## Step-by-Step Explanation (Trivial Version)
-### Code Breakdown
-1. **Import the required urllib modules.**
-```python title="webpagecontentdownloader.py" showLineNumbers{1} {1}
-import urllib.request, urllib.error, urllib.parse
-```
-
-2. **Get user input for URL and filename.**
-```python title="webpagecontentdownloader.py" showLineNumbers{1} {1-2}
-url = input('Enter the URL: ')
-fileName = input('Enter the file name: ')
-```
+```python title="trivial.py"
+import urllib.request
-3. **Make HTTP request and read the response.**
-```python title="webpagecontentdownloader.py" showLineNumbers{1} {1-2}
+url = input("URL: ")
+filename = input("Filename: ")
response = urllib.request.urlopen(url)
-webContent = response.read().decode('utf-8')
+data = response.read().decode("utf-8")
+with open(filename, "w", encoding="utf-8") as f:
+ f.write(data)
```
+- `urlopen` sends a `GET` and returns a file-like object.
+- `read()` slurps the **entire response** into memory.
+- `.decode("utf-8")` turns the bytes into a Python string — **fails for non-UTF-8 pages**, which is most of the legacy web.
+
+This version works only when the URL is reachable, the page is UTF-8, smaller than your RAM, and does not redirect.
-4. **Save the content to a local file.**
-```python title="webpagecontentdownloader.py" showLineNumbers{1} {1-3}
-f = open(fileName, 'w')
-f.write(webContent)
-f.close()
+## Part 2 — Robust Version with `requests`
+
+```bash title="install"
+pip install requests
```
-### How It Works
-1. **URL Input:** User provides the web page URL to download
-2. **HTTP Request:** Program sends a GET request to the specified URL
-3. **Content Retrieval:** Server responds with the web page's HTML content
-4. **Decoding:** Raw bytes are decoded to UTF-8 text format
-5. **File Saving:** Content is written to a user-specified local file
-
-## Features
-- **Simple Interface:** Easy-to-use command-line input
-- **Universal URL Support:** Works with any accessible web page
-- **Custom File Naming:** User specifies the output filename
-- **UTF-8 Encoding:** Properly handles various character sets
-- **Built-in Libraries:** Uses only Python standard library
-- **Lightweight:** Minimal code for maximum functionality
-
-## urllib Library Components
-
-### urllib.request
-- **urlopen():** Opens URLs and returns response objects
-- **Request handling:** Manages HTTP requests
-
-### urllib.error
-- **Exception handling:** Manages URL-related errors
-- **Error types:** HTTPError, URLError
-
-### urllib.parse
-- **URL parsing:** Breaks down URL components
-- **URL encoding:** Handles special characters
-
-## Common Use Cases
-- **Website Backup:** Save local copies of web pages
-- **Content Analysis:** Analyze HTML structure and content
-- **Web Development:** Study other websites' source code
-- **Research:** Collect web data for analysis
-- **Offline Reading:** Download content for offline access
-
-## Sample Downloaded Content
-When you download a web page, you'll get the raw HTML:
-```html
-
-
-
- Example Domain
-
-
-
-
-
-
Example Domain
-
This domain is for use in illustrative examples...
-
-
-
+```python title="robust.py"
+import requests, sys
+from urllib.parse import urlparse
+
+def download(url: str, filename: str | None = None) -> str:
+ if not url.startswith(("http://", "https://")):
+ url = "https://" + url
+ try:
+ r = requests.get(url, timeout=30, headers={"User-Agent": "PCH-Downloader/1.0"})
+ r.raise_for_status()
+ except requests.HTTPError as e:
+ print(f"HTTP {e.response.status_code}: {e.response.reason}")
+ sys.exit(1)
+ except requests.RequestException as e:
+ print(f"Network error: {e}")
+ sys.exit(1)
+
+ filename = filename or (urlparse(url).path.rsplit("/", 1)[-1] or "page.html")
+ encoding = r.encoding or r.apparent_encoding or "utf-8"
+ with open(filename, "w", encoding=encoding) as f:
+ f.write(r.text)
+ print(f"Saved {len(r.text):,} chars to {filename}")
+ return filename
+```
+What this fixes:
+- **Custom User-Agent.** Default `python-requests/...` is widely blocked.
+- **Timeout.** No hanging on slow servers.
+- **`raise_for_status()`** turns 4xx/5xx into exceptions.
+- **`r.encoding` + `r.apparent_encoding`.** First trusts the server's `Content-Type` header; falls back to `chardet`-based guess.
+- **URL auto-completion.** `example.com` → `https://example.com`.
+- **Smart filename.** Derives from URL path if not provided.
+
+## Stream Large Files
+
+`requests.get(...).text` loads everything into RAM. For PDFs, ZIPs, or images, **stream** instead:
+```python title="stream.py"
+def download_large(url: str, filename: str, chunk: int = 8192):
+ with requests.get(url, stream=True, timeout=30) as r:
+ r.raise_for_status()
+ total = int(r.headers.get("Content-Length", 0))
+ done = 0
+ with open(filename, "wb") as f:
+ for piece in r.iter_content(chunk_size=chunk):
+ if not piece: continue
+ f.write(piece)
+ done += len(piece)
+ if total:
+ pct = done * 100 // total
+ print(f"\r{pct}% ({done:,}/{total:,} bytes)", end="", flush=True)
+ print()
+```
+`stream=True` defers the body download. `iter_content` yields it in chunks so memory stays flat regardless of file size.
+
+For a real progress bar, use **`tqdm`**:
+```python title="tqdm.py"
+from tqdm import tqdm
+with requests.get(url, stream=True) as r, open(filename, "wb") as f:
+ bar = tqdm(total=int(r.headers["Content-Length"]), unit="B", unit_scale=True)
+ for chunk in r.iter_content(8192):
+ f.write(chunk); bar.update(len(chunk))
+ bar.close()
```
-## Error Handling Considerations
-The current implementation is basic. Consider adding error handling for:
-- Invalid URLs
-- Network connectivity issues
-- Permission denied errors
-- Encoding problems
+## Retries with Back-off
-## Enhanced Version with Error Handling
-```python title="webpagecontentdownloader.py" showLineNumbers{1}
-import urllib.request, urllib.error, urllib.parse
+Transient errors (5xx, network blips) should retry; permanent ones (404) should not:
+```python title="retry.py"
+from requests.adapters import HTTPAdapter
+from urllib3.util.retry import Retry
-def download_webpage():
- try:
- url = input('Enter the URL: ')
- fileName = input('Enter the file name: ')
-
- # Validate URL format
- if not url.startswith(('http://', 'https://')):
- url = 'https://' + url
-
- response = urllib.request.urlopen(url)
- webContent = response.read().decode('utf-8')
-
- with open(fileName, 'w', encoding='utf-8') as f:
- f.write(webContent)
-
- print(f"Successfully downloaded content to {fileName}")
-
- except urllib.error.HTTPError as e:
- print(f"HTTP Error: {e.code} - {e.reason}")
- except urllib.error.URLError as e:
- print(f"URL Error: {e.reason}")
- except Exception as e:
- print(f"An error occurred: {e}")
+retry = Retry(total=5, backoff_factor=1,
+ status_forcelist=[502, 503, 504], allowed_methods=["GET"])
+adapter = HTTPAdapter(max_retries=retry)
+session = requests.Session()
+session.mount("https://", adapter)
+session.mount("http://", adapter)
+r = session.get(url, timeout=30)
+```
+`backoff_factor=1` means waits of 1, 2, 4, 8, 16 seconds between retries.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `UnicodeDecodeError` | Forced `utf-8` on non-UTF-8 page | Use `r.encoding` / `r.apparent_encoding` |
+| 403 Forbidden | Default User-Agent blocked | Send a real header |
+| Hangs indefinitely | No timeout | `timeout=30` |
+| MemoryError on 4 GB ZIP | `r.text` loads everything | `stream=True` + `iter_content` |
+| File not saved cross-platform | `\` in filename | Use `urllib.parse.quote` or `Path` |
+| Directory traversal | User-supplied filename `"../../etc/passwd"` | Sanitize with `os.path.basename` |
+
+## Variations to Try
+
+### 1. Mirror mode
+Pull the HTML, parse with BeautifulSoup, then download every `
`, `
```
+## WebSockets vs. Threads (vs. HTTP)
+
+| Approach | Model | Scales to many clients? |
+|---|---|---|
+| HTTP polling | Repeated request/response | Poorly — wasteful, laggy |
+| Thread per client | One OS thread each | Hundreds (threads are heavy) |
+| **asyncio + WebSocket** | One loop, many coroutines | Thousands (cheap to suspend) |
+
+This is *why* async exists: I/O-bound work (waiting on the network) suspends cheaply, so one thread serves enormous numbers of idle-but-connected clients.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `DeprecationWarning` on broadcast | `asyncio.wait` with coroutines | Use `asyncio.gather(..., return_exceptions=True)` |
+| Server crashes when one client drops | `send` to a closed socket | `return_exceptions=True`, or `websockets.broadcast` |
+| `handler() takes 1 arg` error | Newer `websockets` drops `path` | Define `handle_client(websocket)` |
+| Messages never arrive | Mixed `ws://` and `wss://`, or wrong port | Match scheme/port; `wss` needs TLS |
+| Dead clients still in the set | Removed only on clean close | Remove in a `finally` block |
+| Blocking call freezes everyone | Using sync I/O in a coroutine | Use async equivalents; never `time.sleep` |
+
+## Variations to Try
+
+1. **Modern broadcast** — the `gather`/`websockets.broadcast` fix (do this first).
+2. **Usernames & system messages** — join/leave notices.
+3. **Chat rooms** — multiple channels.
+4. **Browser UI** — an HTML/JS front end (above).
+5. **Message history** — replay the last N messages on join.
+6. **Typing indicators / presence** — show who's online and typing.
+7. **Authentication** — validate a token on connect.
+8. **Persistence** — log messages to a database.
+
+## Real-World Applications
+- **Live chat & messaging** — Slack, Discord, support widgets.
+- **Collaborative apps** — Google Docs-style live editing.
+- **Live dashboards** — real-time metrics, prices, scores.
+- **Multiplayer games** — low-latency state updates.
+
+## Educational Value
+- **Async Python** — `async`/`await`, coroutines, the event loop.
+- **WebSockets** — persistent, bidirectional connections.
+- **Concurrency models** — async vs. threads vs. polling.
+- **Keeping up with APIs** — recognizing and fixing deprecations.
+
+## Next Steps
+- Replace the deprecated broadcast with **`asyncio.gather`**.
+- Add **usernames**, **system messages**, and **rooms**.
+- Build a **browser client** and add **message history**.
+- Add **authentication** and **persistence**.
+
## Conclusion
-This Real-time Chat Application project is a great way to learn about WebSocket programming and concurrency in Python. You can extend it by adding a GUI, message history, or user authentication.
+You built a real-time chat server with `asyncio` and WebSockets, learned how one thread serves thousands of connections by suspending at `await`, and modernized a deprecated broadcast in the process. Persistent, push-based connections are the backbone of every live app you use — and async is how Python scales them. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/real_time_chat_application.py). Explore more networking projects on Python Central Hub.
diff --git a/src/content/docs/projects/intermediate/real_time_chat_application.mdx b/src/content/docs/projects/intermediate/real_time_chat_application.mdx
deleted file mode 100644
index f5a46762..00000000
--- a/src/content/docs/projects/intermediate/real_time_chat_application.mdx
+++ /dev/null
@@ -1,108 +0,0 @@
----
-title: Real-time Chat Application
-sidebar:
- order: 17
-hero:
- actions:
- - text: View on GitHub
- link: https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/real_time_chat_application.py
- icon: github
- variant: primary
----
-import FileCode from '../../../../components/FileCode.astro'
-
-## Abstract
-Real-time Chat Application is a Python project that enables users to chat in real time over a network. It demonstrates socket programming, threading, and GUI development. This project is ideal for learning about client-server architecture, concurrency, and event-driven programming.
-
-## Prerequisites
-- Python 3.6 or above
-- socket, threading (built-in)
-- tkinter (usually pre-installed)
-
-## Before you Start
-Ensure Python is installed. No external packages are required. For GUI, make sure Tkinter is available.
-
-## Getting Started
-1. Create a folder named `real-time-chat`.
-2. Create a file named `real_time_chat_application.py`.
-3. Copy the code below into your file.
-
-4. Run the server and client scripts as described in the code comments.
-
-## Explanation
-
-### Code Breakdown
-1. **Import required modules.**
-```python
-import socket
-import threading
-import tkinter as tk
-```
-2. **Server setup.**
-```python
-def start_server():
- # Accepts connections and relays messages
- pass
-```
-3. **Client setup.**
-```python
-def start_client():
- # Connects to server and sends/receives messages
- pass
-```
-4. **GUI for chat.**
-```python
-root = tk.Tk()
-root.title('Real-time Chat Application')
-# ...setup widgets for chat display and input...
-root.mainloop()
-```
-
-## Features
-- Real-time text chat
-- Multiple users
-- Simple GUI
-- Demonstrates sockets and threading
-
-## How It Works
-- Server listens for connections
-- Clients connect and send messages
-- Messages are broadcast to all clients
-- GUI displays chat and allows input
-
-## GUI Components
-- **Text area:** Shows chat history
-- **Entry box:** For message input
-- **Send button:** Sends message
-
-## Use Cases
-- Learn networking basics
-- Build simple chat apps
-- Experiment with concurrency
-
-## Next Steps
-You can enhance this project by:
-- Adding user authentication
-- Supporting private messages
-- Improving GUI design
-- Adding emojis or file sharing
-- Implementing message history
-
-## Enhanced Version Ideas
-```python
-def add_authentication():
- # Require login for chat
- pass
-
-def save_chat_history():
- # Store messages in a file
- pass
-```
-
-## Troubleshooting Tips
-- **Connection errors:** Check IP and port
-- **GUI not showing:** Ensure Tkinter is installed
-- **Messages not sent:** Check server/client status
-
-## Conclusion
-This project teaches networking, threading, and GUI basics. Extend it for more features and better user experience.
diff --git a/src/content/docs/projects/intermediate/rest-api-server.mdx b/src/content/docs/projects/intermediate/rest-api-server.mdx
index ef477d4a..fbe2708c 100644
--- a/src/content/docs/projects/intermediate/rest-api-server.mdx
+++ b/src/content/docs/projects/intermediate/rest-api-server.mdx
@@ -1,6 +1,6 @@
---
title: Flask REST API Server with Authentication
-description: Build a comprehensive REST API server with Flask featuring user authentication, database integration, and CRUD operations
+description: Build a production-shaped REST API in Python with Flask — API-key auth, SQLite-backed CRUD for tasks/products/users, pagination, filtering, request logging, usage stats, and JSON error handlers — then learn what to harden for production.
sidebar:
order: 55
hero:
@@ -13,191 +13,193 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+Almost every app you'll build talks to an API — so learning to *build* one is a turning point. This project is a complete REST API in Flask: full CRUD for three resources (tasks, products, users), API-key authentication via a reusable decorator, pagination and filtering on list endpoints, automatic request logging, a usage-statistics endpoint, and consistent JSON error handlers. It's structured the way real APIs are, so the patterns transfer directly to FastAPI, Django REST Framework, or any framework you meet next.
-Create a comprehensive REST API server using Flask that provides user authentication, database integration, CRUD operations, and API key management. This project demonstrates backend development principles, HTTP methods, JSON handling, and secure API design patterns.
+You will leave understanding:
+- What makes an API **RESTful**: resources, HTTP verbs, and status codes.
+- How a **decorator** (`@require_api_key`) cleanly bolts auth onto any endpoint.
+- The CRUD-to-SQL mapping (GET→SELECT, POST→INSERT, PUT→UPDATE, DELETE→DELETE).
+- How pagination, filtering, logging, and error handling work in practice — and what to fix before production.
## Prerequisites
-
-- Python 3.7 or above
-- Text Editor or IDE
-- Solid understanding of Python syntax and OOP concepts
-- Knowledge of HTTP protocols and REST principles
-- Familiarity with databases and SQL operations
-- Understanding of web development and API concepts
-- Basic knowledge of authentication and security practices
+- Python 3.7 or above.
+- A text editor or IDE.
+- `pip install flask flask-cors`.
+- Basic SQL and an understanding of HTTP methods/status codes.
+- A way to make requests: `curl`, Postman, or the `requests` library.
## Getting Started
-### Create a new project
-1. Create a new project folder and name it `flaskRESTAPI`.
-2. Create a new file and name it `flaskrestapi.py`.
-3. Install required dependencies: `pip install flask flask-cors`
-4. Open the project folder in your favorite text editor or IDE.
-5. Copy the code below and paste it into your `flaskrestapi.py` file.
+### Create the project
+1. Create a folder named `flask-rest-api`.
+2. Inside it, create `flaskrestapi.py`.
+3. Install dependencies: `pip install flask flask-cors`.
### Write the code
-1. Add the following code to your `flaskrestapi.py` file.
-
-2. Save the file.
-3. Run the following command to start the API server.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\flaskRESTAPI> python flaskrestapi.py
- * Running on http://127.0.0.1:5000
- * Debug mode: on
-Flask REST API Server Started
-===============================
-Available Endpoints:
-POST /api/users/register - Register new user
-POST /api/users/login - User login
-GET /api/users - Get all users (requires auth)
-GET /api/data - Get data items
-POST /api/data - Create new data item
+
+
+### Run it
+```cmd title="command"
+C:\Users\Your Name\flask-rest-api> python flaskrestapi.py
+# Docs: http://localhost:5000/
+# Health: http://localhost:5000/health
+# Demo key: demo_key_123456789
+```
+Then exercise it:
+```bash title="terminal"
+# List tasks (authenticated)
+curl -H "X-API-Key: demo_key_123456789" http://localhost:5000/api/tasks
+
+# Create a task
+curl -X POST -H "X-API-Key: demo_key_123456789" -H "Content-Type: application/json" \
+ -d '{"title":"Write docs","priority":"high"}' http://localhost:5000/api/tasks
```
-## Explanation
-
-1. The `from flask import Flask, request, jsonify` imports the Flask framework for web development.
-2. The `flask_cors` enables Cross-Origin Resource Sharing for API accessibility.
-3. The `sqlite3` module provides database functionality for data persistence.
-4. The `DatabaseManager` class handles all database operations and table creation.
-5. User authentication uses password hashing and API key generation for security.
-6. CRUD operations (Create, Read, Update, Delete) manage data through HTTP methods.
-7. JSON responses ensure standardized data exchange between client and server.
-8. Error handling provides meaningful HTTP status codes and error messages.
-9. API key authentication secures endpoints and validates user permissions.
-10. Logging functionality tracks API usage and debugging information.
-11. Route decorators define URL endpoints and acceptable HTTP methods.
-12. Request validation ensures data integrity and prevents malformed requests.
-
-## Next Steps
-
-Congratulations! You have successfully created a Flask REST API Server in Python. Experiment with the code and see if you can modify the application. Here are a few suggestions:
-- Add JWT token-based authentication
-- Implement rate limiting and API throttling
-- Create comprehensive API documentation with Swagger
-- Add data validation and serialization
-- Implement file upload and download endpoints
-- Create real-time features with WebSockets
-- Add caching mechanisms for improved performance
-- Implement API versioning strategies
-- Create automated testing suites for endpoints
+## REST in One Minute
-## Conclusion
+A REST API exposes **resources** (tasks, products) at URLs, and uses HTTP **verbs** to act on them:
-In this project, you learned how to create a Flask REST API Server in Python with authentication and database integration. You also learned about HTTP protocols, JSON handling, security practices, and building scalable backend services. You can find the source code on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/flaskrestapi.py)
+| Verb | URL | Action | SQL | Success code |
+|---|---|---|---|---|
+| GET | `/api/tasks` | List | `SELECT` | 200 |
+| GET | `/api/tasks/` | Read one | `SELECT … WHERE id` | 200 |
+| POST | `/api/tasks` | Create | `INSERT` | 201 |
+| PUT | `/api/tasks/` | Update | `UPDATE` | 200 |
+| DELETE | `/api/tasks/` | Delete | `DELETE` | 204 |
-## Code Explanation
+Learn this table and you've learned the shape of every CRUD API.
-### Flask Application Setup
-```python title="restapiserver.py" showLineNumbers{1}
-from flask import Flask, request, jsonify
-from flask_cors import CORS
+## Step-by-Step Explanation
+### 1. Setup and CORS
+```python title="flaskrestapi.py"
app = Flask(__name__)
-CORS(app) # Enable CORS for all routes
-app.config['SECRET_KEY'] = 'your-secret-key-here'
+CORS(app) # let browser front-ends on other origins call this API
```
-
-Initializes the Flask application with CORS support and configuration settings.
-
-### Database Management
-```python title="restapiserver.py" showLineNumbers{1}
-class DatabaseManager:
- def init_database(self):
- with sqlite3.connect(self.db_path) as conn:
- cursor = conn.cursor()
- cursor.execute('''CREATE TABLE IF NOT EXISTS users...''')
-```
-
-Manages database connections, table creation, and data operations with SQLite integration.
-
-### User Authentication
-```python title="restapiserver.py" showLineNumbers{1}
-def hash_password(password):
- salt = secrets.token_hex(16)
- password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
- return f"{salt}:{password_hash}"
-
-def verify_password(password, stored_hash):
- salt, hash_value = stored_hash.split(':')
- return hashlib.sha256((password + salt).encode()).hexdigest() == hash_value
+`flask-cors` adds the headers a browser needs to call your API from a different origin (e.g. a React app on `localhost:3000`). Without it, the browser blocks the request.
+
+### 2. The database layer
+`DatabaseManager` creates five tables — `users`, `api_keys`, `tasks`, `products`, `api_logs` — and seeds sample data. Foreign keys link API keys and tasks to users. Note `conn.row_factory = sqlite3.Row`, which lets rows convert to dicts so they `jsonify` cleanly:
+```python title="flaskrestapi.py"
+def get_db_connection():
+ conn = sqlite3.connect(DATABASE)
+ conn.row_factory = sqlite3.Row # rows behave like dicts -> dict(row)
+ return conn
```
-Implements secure password hashing and verification using salt and SHA-256.
-
-### API Key Authentication
-```python title="restapiserver.py" showLineNumbers{1}
+### 3. Authentication as a decorator
+This is the centerpiece. Instead of repeating auth in every endpoint, wrap it once:
+```python title="flaskrestapi.py"
def require_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
- api_key = request.headers.get('X-API-Key')
- if not api_key or not db_manager.verify_api_key(api_key):
+ api_key = request.headers.get('X-API-Key') or request.args.get('api_key')
+ if not api_key:
+ return jsonify({'error': 'API key required'}), 401
+ # look the key up, join to its user, check both are active...
+ if not result:
return jsonify({'error': 'Invalid API key'}), 401
+ request.current_user_id = result[1] # stash user on the request
return f(*args, **kwargs)
return decorated_function
```
-
-Protects API endpoints with decorator-based authentication using API keys.
-
-### CRUD Operations
-```python title="restapiserver.py" showLineNumbers{1}
-@app.route('/api/data', methods=['GET'])
-def get_data():
- data = db_manager.get_all_data()
- return jsonify({'data': data, 'count': len(data)})
-
-@app.route('/api/data', methods=['POST'])
-@require_api_key
-def create_data():
- data = request.get_json()
- result = db_manager.create_data_item(data)
- return jsonify(result), 201
+Slap `@require_api_key` on any route and it's protected. The decorator also records `last_used` and attaches the authenticated user to `request`, so handlers know who's calling. This is exactly how production APIs centralize auth.
+
+### 4. List endpoint with filtering + pagination
+```python title="flaskrestapi.py"
+query = "SELECT * FROM tasks WHERE 1=1"
+params = []
+if completed is not None:
+ query += " AND completed = ?"; params.append(completed)
+if priority:
+ query += " AND priority = ?"; params.append(priority)
+query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
+params.extend([per_page, (page - 1) * per_page])
```
+The `WHERE 1=1` trick lets you append `AND …` clauses conditionally without worrying about whether it's the first one. `LIMIT`/`OFFSET` implement pagination, and `per_page` is capped (`min(..., 100)`) so a client can't request a million rows. Every value goes through `?` placeholders — no SQL injection.
+
+### 5. Create with validation
+```python title="flaskrestapi.py"
+data = request.get_json()
+if not data or 'title' not in data:
+ return jsonify({'error': 'Title is required'}), 400
+if priority not in ['low', 'medium', 'high', 'critical']:
+ return jsonify({'error': 'Invalid priority...'}), 400
+# INSERT ... then return the created row with 201
+return jsonify(dict(task)), 201
+```
+Validate **before** touching the database, return `400` with a clear message on bad input, and `201 Created` (not 200) with the new resource on success. Returning the created object saves the client a follow-up GET.
+
+### 6. Partial update (PATCH-style PUT)
+```python title="flaskrestapi.py"
+for field in ['title', 'description', 'completed', 'priority', 'due_date']:
+ if field in data:
+ update_fields.append(f"{field} = ?")
+ params.append(data[field])
+query = f"UPDATE tasks SET {', '.join(update_fields)} WHERE id = ?"
+```
+Only the fields present in the request body get updated — send just `{"completed": true}` and nothing else changes. (The field *names* are hard-coded in code, not taken from input, so this dynamic SQL is safe.)
-Implements Create, Read, Update, Delete operations through HTTP methods and JSON responses.
-
-## Features
+### 7. Consistent error handling
+```python title="flaskrestapi.py"
+@app.errorhandler(404)
+def not_found(error):
+ return jsonify({'error': 'Resource not found'}), 404
+```
+Registering JSON error handlers means clients always get JSON — never an HTML error page that breaks their parser. An API that sometimes returns HTML is a debugging nightmare.
+
+## Before Production: What to Fix
+
+The code is a great teaching API but makes demo-grade choices:
+
+| Issue | Why it matters | Fix |
+|---|---|---|
+| `hashlib.sha256(password)` | Too fast → brute-forceable; no salt | Use `bcrypt`/`argon2` |
+| API keys stored in plaintext | DB leak exposes all keys | Store **hashes** of keys |
+| `debug=True, host='0.0.0.0'` | Debug RCE, exposed to network | `debug=False`; bind appropriately |
+| Hard-coded `SECRET_KEY` | Forgeable | Load from env var |
+| No rate limiting | Abuse / DoS | Add `flask-limiter` |
+| Logging stores the API key | Sensitive data in logs | Log a key *id*, not the key |
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `401` on every call | Missing/!active key, or wrong header | Send `X-API-Key`; use a seeded demo key |
+| `415 Unsupported Media Type` | No `Content-Type: application/json` | Set the header on POST/PUT |
+| `Object of type Row is not JSON serializable` | Forgot to `dict(row)` | Convert rows before `jsonify` |
+| Returns HTML on errors | No JSON error handlers | Register `@app.errorhandler` for 400/404/500 |
+| SQL injection risk | String-built queries with input | Use `?` placeholders (the code does) |
+| Browser front-end blocked | CORS not enabled | `CORS(app)` (the code does) |
+
+## Variations to Try
+
+1. **JWT auth** — swap API keys for token-based login with expiry.
+2. **OpenAPI/Swagger** — auto-generate interactive docs.
+3. **Rate limiting** — `flask-limiter` per key.
+4. **Soft deletes** — flag `is_active` instead of `DELETE`.
+5. **Search & sort** — `?q=` full-text and `?sort=` parameters.
+6. **Versioning** — `/api/v1/...` to evolve without breaking clients.
+7. **Tests** — pytest hitting each endpoint with the test client.
+8. **Port to FastAPI** — compare async + automatic docs.
+
+## Real-World Applications
+- **Backends for web/mobile apps** — the server half of any SPA.
+- **Microservices** — small, focused services behind a gateway.
+- **Public/partner APIs** — key-authenticated third-party access.
+- **Internal tooling** — data services for dashboards and automations.
-- **User Registration & Authentication**: Secure user account management with hashed passwords
-- **API Key Management**: Generate and validate API keys for endpoint access
-- **Database Integration**: SQLite database with automated table creation and management
-- **CRUD Operations**: Complete Create, Read, Update, Delete functionality
-- **JSON API Responses**: Standardized JSON responses with proper HTTP status codes
-- **Error Handling**: Comprehensive error management with meaningful error messages
-- **CORS Support**: Cross-Origin Resource Sharing for web application integration
-- **Request Validation**: Input validation and data sanitization for security
+## Educational Value
+- **REST design** — resources, verbs, status codes, idempotency.
+- **Decorators** — cross-cutting concerns (auth, logging) done right.
+- **Database-backed CRUD** — safe parameterized SQL end to end.
+- **API operability** — pagination, logging, stats, error contracts.
## Next Steps
+- Exercise every endpoint with `curl`/Postman.
+- Add **JWT auth** and **rate limiting**.
+- Generate **Swagger** docs and write **pytest** tests.
+- Harden the **production** issues above.
-### Enhancements
-- Add JWT token-based authentication
-- Implement rate limiting and API throttling
-- Create comprehensive API documentation with Swagger
-- Add data validation and serialization with Marshmallow
-- Implement file upload and download endpoints
-- Create real-time features with WebSockets
-- Add caching mechanisms for improved performance
-- Implement API versioning strategies
-
-### Learning Extensions
-- Study RESTful API design principles
-- Explore microservices architecture patterns
-- Learn about API security best practices
-- Practice with database optimization techniques
-- Understand containerization with Docker
-- Explore cloud deployment strategies
-
-## Educational Value
-
-This project teaches:
-- **Backend Development**: Building server-side applications with Flask framework
-- **REST API Design**: Understanding HTTP methods, status codes, and resource modeling
-- **Database Integration**: Working with SQLite and SQL operations
-- **Authentication & Security**: Implementing secure user authentication and API protection
-- **JSON Handling**: Processing and returning JSON data in web applications
-- **Error Handling**: Managing exceptions and providing meaningful error responses
-- **Web Protocols**: Understanding HTTP requests, responses, and headers
-- **API Documentation**: Creating clear documentation for API endpoints
-
-Perfect for understanding backend development, web API creation, and server-side programming with Python.
-````
+## Conclusion
+You built a real REST API — authenticated, paginated, filtered, logged, and consistently error-handled — and learned the resource/verb/status-code model that underlies every backend. The `@require_api_key` decorator and parameterized CRUD are patterns you'll reuse for the rest of your career; the production-hardening table is what separates a tutorial API from a shippable one. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/flaskrestapi.py). Explore more backend projects on Python Central Hub.
diff --git a/src/content/docs/projects/intermediate/simple-blog-with-flask.mdx b/src/content/docs/projects/intermediate/simple-blog-with-flask.mdx
index 9b3d22a5..be48c525 100644
--- a/src/content/docs/projects/intermediate/simple-blog-with-flask.mdx
+++ b/src/content/docs/projects/intermediate/simple-blog-with-flask.mdx
@@ -1,5 +1,6 @@
---
title: Simple Blog with Flask
+description: Build a blog in Python with Flask — list posts, view individual posts, and add new ones via a form — then make it persistent with a SQLite database, add templates, edit/delete, and basic validation.
sidebar:
order: 93
hero:
@@ -12,46 +13,85 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+A blog is the "to-do app" of web frameworks — small enough to finish, complete enough to teach everything. This Flask project does the full CRUD-lite loop: list posts, view a single post by ID, and submit new posts through an HTML form. You'll learn routing, dynamic URLs, form handling with GET vs. POST, and Jinja templating. Then you'll fix the elephant in the room — posts vanish on restart because they live in a list — by moving to a SQLite database, and round it out with edit, delete, and validation.
-Create a Simple Blog application using Flask. The app displays a list of blog posts, allows adding new posts, and viewing individual posts. This project demonstrates web development, routing, and template rendering in Python.
+You will leave understanding:
+- How Flask routes URLs to view functions, including dynamic ``.
+- The GET-to-show-form, POST-to-submit pattern and why you redirect after POST.
+- How Jinja templates render data passed from views.
+- Why in-memory storage fails and how a database fixes it.
## Prerequisites
-- Python 3.6 or above
-- Text Editor or IDE
-- Basic understanding of Python syntax
-- Familiarity with Flask for web development
-- Knowledge of HTML templates
+- Python 3.6 or above.
+- A text editor or IDE.
+- Flask: `pip install flask`.
+- Basic HTML.
+- Understanding of functions, lists, and dicts.
## Getting Started
-### Creating a new project
-1. Create a new project folder and name it `simple_blog_with_flask`.
-2. Create a new file inside the folder and name it `simple_blog_with_flask.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into the `simple_blog_with_flask.py` file.
+
+### Create the project
+1. Create a folder named `flask-blog` with a `templates/` subfolder.
+2. Inside it, create `simple_blog_with_flask.py`.
+3. Install Flask: `pip install flask`.
+
+### Templates
+Flask renders HTML from the `templates/` folder:
+```html title="templates/index.html"
+Blog
+New Post
+
+```
+```html title="templates/post.html"
+{{ post.title }}
+{{ post.content }}
+Back
+```
+```html title="templates/new_post.html"
+New Post
+
+```
### Write the code
-## Key Features
-- Display a list of blog posts
-- Add new blog posts
-- View individual blog posts
-- Web interface using Flask and HTML templates
-
-## Explanation
-
-### Displaying Blog Posts
-The app uses a list of dictionaries to store blog posts and renders them using Flask templates:
-```python title="simple_blog_with_flask.py" showLineNumbers={1}
-blog_posts = [
- {"id": 1, "title": "First Post", "content": "This is the content of the first post."},
- {"id": 2, "title": "Second Post", "content": "This is the content of the second post."},
-]
+### Run it
+```cmd title="command"
+C:\Users\Your Name\flask-blog> python simple_blog_with_flask.py
+# Open http://127.0.0.1:5000 — see posts, click one, or add a new one.
```
-### Adding New Posts
-Users can add new posts via a form. The new post is appended to the list and the user is redirected to the main page:
-```python title="simple_blog_with_flask.py" showLineNumbers={1}
+## Step-by-Step Explanation
+
+### 1. Listing posts
+```python title="simple_blog_with_flask.py"
+@app.route('/')
+def index():
+ return render_template('index.html', posts=blog_posts)
+```
+The home route passes the `blog_posts` list into `index.html`, where Jinja loops over it. Data flows from Python to HTML through `render_template` keyword arguments.
+
+### 2. Viewing one post (dynamic URL)
+```python title="simple_blog_with_flask.py"
+@app.route('/post/')
+def view_post(post_id):
+ post = next((p for p in blog_posts if p['id'] == post_id), None)
+ if post:
+ return render_template('post.html', post=post)
+ return "Post not found", 404
+```
+`` captures the number from `/post/2` and passes it in. The `next(...)` finds the matching post; if none, returning a string + `404` sends a proper Not Found status. Handling the missing case is what separates robust routes from fragile ones.
+
+### 3. Adding a post (GET vs POST)
+```python title="simple_blog_with_flask.py"
@app.route('/new', methods=['GET', 'POST'])
def new_post():
if request.method == 'POST':
@@ -62,28 +102,94 @@ def new_post():
return redirect(url_for('index'))
return render_template('new_post.html')
```
+One route, two behaviors: a **GET** shows the empty form, a **POST** processes the submission. After saving, it **redirects** to the home page — the Post/Redirect/Get pattern that prevents a refresh from re-submitting the post. The `new_id` line generates the next ID.
-### Viewing Individual Posts
-Each post can be viewed by visiting its unique URL:
-```python title="simple_blog_with_flask.py" showLineNumbers={1}
-@app.route('/post/')
-def view_post(post_id):
- post = next((p for p in blog_posts if p['id'] == post_id), None)
- if post:
- return render_template('post.html', post=post)
- return "Post not found", 404
-```
+## The Real Problem: Posts Don't Survive Restart
+
+`blog_posts` is a Python list in memory — restart the server and every post is gone. Move storage to SQLite:
+```python title="db.py"
+import sqlite3
+from flask import g
-## Running the Application
-1. Save the file.
-2. Install Flask:
-```bash
-pip install flask
+def get_db():
+ if "db" not in g:
+ g.db = sqlite3.connect("blog.db")
+ g.db.row_factory = sqlite3.Row # rows behave like dicts
+ return g.db
+
+# one-time setup:
+# CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT);
+
+def all_posts():
+ return get_db().execute("SELECT * FROM posts").fetchall()
+
+def add_post(title, content):
+ db = get_db()
+ db.execute("INSERT INTO posts (title, content) VALUES (?, ?)", (title, content))
+ db.commit()
```
-3. Run the application:
-```bash
-python simple_blog_with_flask.py
+`AUTOINCREMENT` replaces the manual `new_id`, and `?` placeholders keep you safe from SQL injection. Now posts persist.
+
+## Add Edit and Delete
+
+Complete the CRUD set:
+```python title="crud.py"
+@app.route('/edit/', methods=['GET', 'POST'])
+def edit_post(post_id):
+ if request.method == 'POST':
+ get_db().execute("UPDATE posts SET title=?, content=? WHERE id=?",
+ (request.form['title'], request.form['content'], post_id))
+ get_db().commit()
+ return redirect(url_for('view_post', post_id=post_id))
+ post = get_db().execute("SELECT * FROM posts WHERE id=?", (post_id,)).fetchone()
+ return render_template('edit_post.html', post=post)
+
+@app.route('/delete/', methods=['POST'])
+def delete_post(post_id):
+ get_db().execute("DELETE FROM posts WHERE id=?", (post_id,))
+ get_db().commit()
+ return redirect(url_for('index'))
```
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| Posts disappear on restart | Stored in a Python list | Persist to **SQLite** |
+| `TemplateNotFound` | HTML not in `templates/` | Use the `templates/` folder |
+| `400 Bad Request` on submit | `request.form['x']` missing key | Match form `name=` attributes; use `.get()` |
+| Refresh re-posts the entry | No redirect after POST | Redirect (PRG pattern) |
+| Crash on `/post/999` | Assumed the post exists | Handle `None` → return 404 |
+| HTML shows as raw tags | Auto-escaping confusion | Trust Jinja's escaping; don't disable it |
+
+## Variations to Try
+
+1. **SQLite persistence** — the essential upgrade above.
+2. **Edit & delete** — full CRUD.
+3. **Markdown posts** — render content with a Markdown library.
+4. **Comments** — a second table linked to posts.
+5. **Authentication** — login so only you can post (see [REST API with Authentication](./rest-api-auth)).
+6. **Pagination** — `LIMIT`/`OFFSET` for many posts.
+7. **Tags & search** — categorize and filter posts.
+8. **Base template** — extract a shared layout with Jinja `{% extends %}`.
+
+## Real-World Applications
+- **Blogs & CMSs** — WordPress and Ghost are this idea, scaled up.
+- **Documentation sites** — content lists + detail pages.
+- **Internal wikis** — team knowledge bases.
+- **Learning Flask** — the canonical first real web app.
+
+## Educational Value
+- **Routing** — static and dynamic URLs.
+- **Forms** — GET/POST, `request.form`, and the PRG pattern.
+- **Templating** — passing data to Jinja, loops, `url_for`.
+- **Persistence** — moving from memory to a real database.
+
+## Next Steps
+- Move posts into **SQLite** so they persist.
+- Add **edit** and **delete** routes.
+- Render content as **Markdown**; add **comments**.
+- Add **authentication** and **pagination**.
+
## Conclusion
-This Simple Blog with Flask project is a great way to learn about web development and routing in Python. You can extend it by adding user authentication, comments, or a database backend.
+You built a Flask blog that lists, displays, and creates posts — and learned routing, forms, templating, and the PRG pattern along the way. Fixing the memory-storage flaw with SQLite and adding edit/delete turns it into a genuine little CMS. This is the foundation every Flask developer builds on. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/simple_blog_with_flask.py). Explore more web projects on Python Central Hub.
diff --git a/src/content/docs/projects/intermediate/simple-weather-forecast-app.mdx b/src/content/docs/projects/intermediate/simple-weather-forecast-app.mdx
index f752692c..6a1e0448 100644
--- a/src/content/docs/projects/intermediate/simple-weather-forecast-app.mdx
+++ b/src/content/docs/projects/intermediate/simple-weather-forecast-app.mdx
@@ -1,5 +1,6 @@
---
title: Simple Weather Forecast App
+description: Build a Tkinter weather app in Python using the OpenWeatherMap API — fetch current conditions for any city, then add a 5-day forecast, weather icons, unit toggles, geolocation, caching, and graceful error handling.
sidebar:
order: 100
hero:
@@ -12,56 +13,168 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+Consuming a real web API is a rite of passage, and weather is the perfect first API: free, well-documented, and instantly gratifying. In this tutorial you build a Tkinter app that queries the OpenWeatherMap API for a city's current conditions and displays temperature, "feels like", humidity, and a description. Then you grow it into something polished — a 5-day forecast, weather icons, °C/°F toggle, geolocation, response caching, and the kind of error handling that separates a demo from a tool.
-Build a Simple Weather Forecast App that fetches and displays weather forecast data from an API. The app provides a GUI for entering city names and viewing the forecast. This project demonstrates API integration and GUI development in Python.
+You will leave understanding:
+- How to register for and use an API key safely.
+- The request → JSON → parse → display pipeline behind every API client.
+- How to read nested JSON (`data["weather"][0]["description"]`) without crashing.
+- Why you check `status_code` and handle the unhappy paths first.
## Prerequisites
-- Python 3.6 or above
-- Text Editor or IDE
-- Basic understanding of Python syntax
-- Familiarity with Tkinter for GUI development
-- Knowledge of API requests
+- Python 3.6 or above.
+- A text editor or IDE.
+- The `requests` library: `pip install requests`.
+- A **free** OpenWeatherMap API key from [openweathermap.org/api](https://openweathermap.org/api).
+- Tkinter (bundled with Python).
## Getting Started
-### Creating a new project
-1. Create a new project folder and name it `simple_weather_forecast_app`.
-2. Create a new file inside the folder and name it `simple_weather_forecast_app.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into the `simple_weather_forecast_app.py` file.
+
+### Get an API key
+1. Sign up at [openweathermap.org](https://openweathermap.org/api) (free tier is plenty).
+2. Copy your API key from the dashboard.
+3. New keys can take a few minutes to activate — don't panic if the first call 401s.
+
+### Create the project
+1. Create a folder named `weather-app`.
+2. Inside it, create `simple_weather_forecast_app.py`.
+3. Install the dependency: `pip install requests`.
### Write the code
-## Key Features
-- Fetch weather data from an API
-- Display forecast in a user-friendly format
-- GUI interface for user interaction
+### Run it
+```cmd title="command"
+C:\Users\Your Name\weather-app> python simple_weather_forecast_app.py
+# Enter a city name, click "Get Weather".
+# Make sure you replaced API_KEY with your real key first.
+```
-## Explanation
+## Step-by-Step Explanation
-### Fetching Weather Data
-Weather data is fetched from the OpenWeatherMap API:
-```python title="simple_weather_forecast_app.py" showLineNumbers={1}
+### 1. The endpoint and key
+```python title="simple_weather_forecast_app.py"
+API_KEY = "your_openweathermap_api_key"
+BASE_URL = "http://api.openweathermap.org/data/2.5/weather"
+```
+`BASE_URL` is the "current weather" endpoint. The key authenticates you and counts against your rate limit.
+
+### 2. Building the request
+```python title="simple_weather_forecast_app.py"
params = {"q": city, "appid": API_KEY, "units": "metric"}
response = requests.get(BASE_URL, params=params)
+data = response.json()
+```
+Passing a `params` dict lets `requests` build the query string for you (`?q=London&appid=...&units=metric`) and handles URL-encoding spaces and special characters. `units="metric"` gives Celsius; `"imperial"` gives Fahrenheit.
+
+### 3. Check status before parsing
+```python title="simple_weather_forecast_app.py"
+if response.status_code == 200:
+ weather = data["weather"][0]["description"].capitalize()
+ temp = data["main"]["temp"]
+ ...
+else:
+ messagebox.showerror("Error", data.get("message", "Failed to fetch weather data."))
```
+This is the most important habit in API work: **handle the failure path first**. A typo'd city returns `404` with a helpful `message` field — surface it instead of crashing on a missing key.
-### Displaying Results
-Weather information is shown in a Tkinter Label widget:
-```python title="simple_weather_forecast_app.py" showLineNumbers={1}
-self.result_label.config(text=f"Weather: {weather}\nTemperature: {temp}°C")
+### 4. Reading nested JSON
+```python title="simple_weather_forecast_app.py"
+data["weather"][0]["description"] # list → first item → field
+data["main"]["temp"] # nested dict
```
+OpenWeatherMap nests `weather` inside a *list* (a place can have several conditions) and groups numbers under `main`. Knowing the shape of the JSON is half the battle — print `data` once and study it.
-## Running the Application
-1. Save the file.
-2. Install required dependencies:
-```bash
-pip install requests
+## Protect Your API Key
+
+Never hard-code secrets. Use an environment variable:
+```python title="config.py"
+import os
+API_KEY = os.environ["OWM_API_KEY"] # set OWM_API_KEY in your shell, not in code
```
-3. Run the application:
-```bash
-python simple_weather_forecast_app.py
+```cmd title="command"
+# PowerShell
+$env:OWM_API_KEY = "your_key_here"
```
+This keeps the key out of screenshots, git history, and shared code.
+
+## Add a 5-Day Forecast
+
+The forecast endpoint returns data in 3-hour steps:
+```python title="forecast.py"
+FORECAST_URL = "http://api.openweathermap.org/data/2.5/forecast"
+
+def five_day(city):
+ r = requests.get(FORECAST_URL,
+ params={"q": city, "appid": API_KEY, "units": "metric"},
+ timeout=10)
+ r.raise_for_status()
+ # Pick the midday (12:00) reading for each day
+ return [item for item in r.json()["list"] if "12:00:00" in item["dt_txt"]]
+```
+Loop the result into five labels or a small table.
+
+## Show Weather Icons
+
+Each condition carries an icon code:
+```python title="icons.py"
+from tkinter import PhotoImage
+import requests, io
+
+icon_code = data["weather"][0]["icon"] # e.g. "10d"
+url = f"http://openweathermap.org/img/wn/{icon_code}@2x.png"
+img_bytes = requests.get(url, timeout=10).content
+# Save then load, or use Pillow's ImageTk for in-memory display
+```
+
+## Add a °C / °F Toggle
+
+Store the raw value once, convert for display:
+```python title="units.py"
+def to_fahrenheit(celsius):
+ return celsius * 9 / 5 + 32
+
+label.config(text=f"{temp}°C" if self.metric else f"{to_fahrenheit(temp):.1f}°F")
+```
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `401 Unauthorized` | Key missing, wrong, or not yet active | Check the key; wait a few minutes after signup |
+| `KeyError: 'main'` | Parsed JSON before checking status | Guard with `if status_code == 200` first |
+| App freezes on slow network | No `timeout` | Add `timeout=10`; fetch on a background thread |
+| Spaces in city name break URL | Manual string concatenation | Use the `params=` dict — it encodes for you |
+| Wrong temperature scale | Forgot `units` | Set `units="metric"` or `"imperial"` |
+| Rate-limit errors | Polling too often | Cache responses for a few minutes |
+
+## Variations to Try
+
+1. **Geolocation** — detect the user's city via their IP and load it on launch.
+2. **Hourly chart** — plot the next 24 hours with `matplotlib`.
+3. **Favorites** — save a list of cities and refresh them all at once.
+4. **Severe-weather alerts** — use the One Call API's `alerts` field.
+5. **Background by condition** — change the window color for sunny/rainy/snow.
+6. **Caching layer** — store responses with a timestamp; skip the API within 10 minutes.
+7. **Voice output** — read the forecast aloud (see [Weather App with Voice Commands](./weather-app-with-voice-commands)).
+
+## Real-World Applications
+- **Travel apps** — destination weather at a glance.
+- **Agriculture & logistics** — planning around conditions.
+- **Dashboards** — a weather widget on a home-automation panel.
+- **Event planning** — outdoor-event go/no-go decisions.
+
+## Educational Value
+- **REST APIs** — keys, query parameters, JSON responses.
+- **Defensive parsing** — status checks and `.get()` with defaults.
+- **Secret management** — environment variables over hard-coding.
+- **Caching & rate limits** — being a good API citizen.
+
+## Next Steps
+- Move the key into an **environment variable**.
+- Add a **5-day forecast** and **weather icons**.
+- Implement a **°C/°F toggle** and **response caching**.
+- Detect the user's city with **geolocation**.
## Conclusion
-This Simple Weather Forecast App project is a great way to learn about API integration and GUI development in Python. You can extend it by adding more detailed forecasts, weather maps, or historical data.
+You built a weather app that turns a city name into live conditions, then layered on a forecast, icons, unit toggles, and caching — all on top of the same request → parse → display loop you'll reuse for every API you ever touch. The skill transfers directly: swap the endpoint and you have a stock ticker, a currency converter, or a news reader. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/simple_weather_forecast_app.py). Explore more API projects on Python Central Hub.
diff --git a/src/content/docs/projects/intermediate/simple_blog_with_flask.mdx b/src/content/docs/projects/intermediate/simple_blog_with_flask.mdx
deleted file mode 100644
index 6da4c44c..00000000
--- a/src/content/docs/projects/intermediate/simple_blog_with_flask.mdx
+++ /dev/null
@@ -1,106 +0,0 @@
----
-title: Simple Blog with Flask
-sidebar:
- order: 6
-hero:
- actions:
- - text: View on GitHub
- link: https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/simple_blog_with_flask.py
- icon: github
- variant: primary
----
-import FileCode from '../../../../components/FileCode.astro'
-
-## Abstract
-Simple Blog with Flask is a Python web application that allows users to create, view, and manage blog posts. It demonstrates web development, routing, and template rendering with Flask. This project is ideal for learning about backend development, HTTP requests, and dynamic web pages.
-
-## Prerequisites
-- Python 3.6 or above
-- Flask (`pip install flask`)
-
-## Before you Start
-Install Python and Flask. Familiarize yourself with basic web concepts.
-
-## Getting Started
-1. Create a folder named `simple-blog`.
-2. Create a file named `simple_blog_with_flask.py`.
-3. Copy the code below into your file.
-
-4. Run the script: `python simple_blog_with_flask.py`
-
-## Explanation
-
-### Code Breakdown
-1. **Import modules**
-```python
-from flask import Flask, render_template, request, redirect
-```
-2. **Set up Flask app**
-```python
-app = Flask(__name__)
-```
-3. **Define routes**
-```python
-@app.route('/')
-def home():
- # Show all posts
- pass
-
-@app.route('/add', methods=['POST'])
-def add_post():
- # Add new post
- pass
-```
-4. **Run the app**
-```python
-if __name__ == '__main__':
- app.run(debug=True)
-```
-
-## Features
-- Create and view blog posts
-- Simple web interface
-- Demonstrates Flask routing
-- Easy to extend for more features
-
-## How It Works
-- Handles HTTP requests
-- Renders templates for pages
-- Stores posts in memory or file
-
-## GUI Components
-- **Home page:** Lists posts
-- **Form:** For adding new posts
-- **Buttons:** For navigation
-
-## Use Cases
-- Personal blogging
-- Learning web development
-- Building portfolio sites
-
-## Next Steps
-You can enhance this project by:
-- Adding user authentication
-- Storing posts in a database
-- Supporting comments
-- Improving UI with CSS
-- Deploying online
-
-## Enhanced Version Ideas
-```python
-def add_database_support():
- # Use SQLite or PostgreSQL
- pass
-
-def add_user_login():
- # Require login for posting
- pass
-```
-
-## Troubleshooting Tips
-- **Flask errors:** Check installation
-- **App not running:** Check port and debug mode
-- **Posts not saving:** Check storage logic
-
-## Conclusion
-This project teaches Flask basics, routing, and web app structure. Extend it for more features and production use.
diff --git a/src/content/docs/projects/intermediate/simple_weather_forecast_app.mdx b/src/content/docs/projects/intermediate/simple_weather_forecast_app.mdx
deleted file mode 100644
index fe3c0e1e..00000000
--- a/src/content/docs/projects/intermediate/simple_weather_forecast_app.mdx
+++ /dev/null
@@ -1,97 +0,0 @@
----
-title: Simple Weather Forecast App
-sidebar:
- order: 13
-hero:
- actions:
- - text: View on GitHub
- link: https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/simple_weather_forecast_app.py
- icon: github
- variant: primary
----
-import FileCode from '../../../../components/FileCode.astro'
-
-## Abstract
-Simple Weather Forecast App is a Python project that fetches and displays weather forecasts for a given city. It demonstrates API integration, data parsing, and GUI development. This project is ideal for learning about REST APIs, JSON handling, and interactive desktop apps.
-
-## Prerequisites
-- Python 3.6 or above
-- requests (`pip install requests`)
-- tkinter (usually pre-installed)
-
-## Before you Start
-Install Python and requests. Obtain a free API key from a weather service (e.g., OpenWeatherMap).
-
-## Getting Started
-1. Create a folder named `weather-forecast-app`.
-2. Create a file named `simple_weather_forecast_app.py`.
-3. Copy the code below into your file.
-
-4. Run the script: `python simple_weather_forecast_app.py`
-
-## Explanation
-
-### Code Breakdown
-1. **Import modules**
-```python
-import requests
-import tkinter as tk
-```
-2. **Fetch weather data**
-```python
-def get_forecast(city):
- # Fetch weather data from API
- pass
-```
-3. **GUI for forecast display**
-```python
-root = tk.Tk()
-root.title('Weather Forecast App')
-# ...setup widgets for city input and forecast display...
-root.mainloop()
-```
-
-## Features
-- Fetches weather forecast from API
-- GUI for display
-- Easy to extend for more features
-
-## How It Works
-- User enters city name
-- App fetches and displays forecast
-
-## GUI Components
-- **Label:** Shows forecast info
-- **Entry box:** For city input
-- **Button:** Fetches forecast
-
-## Use Cases
-- Daily weather updates
-- Learn API integration
-- Build custom weather dashboards
-
-## Next Steps
-You can enhance this project by:
-- Adding more weather details
-- Supporting multiple cities
-- Improving GUI design
-- Adding charts or graphs
-
-## Enhanced Version Ideas
-```python
-def add_graphs():
- # Display weather trends
- pass
-
-def support_multiple_cities():
- # Show forecasts for several cities
- pass
-```
-
-## Troubleshooting Tips
-- **API key errors:** Check your key
-- **No forecast displayed:** Check city name and internet connection
-- **GUI not showing:** Ensure Tkinter is installed
-
-## Conclusion
-This project teaches API usage, JSON parsing, and GUI basics. Extend it for more features and better user experience.
diff --git a/src/content/docs/projects/intermediate/social-media-analytics.mdx b/src/content/docs/projects/intermediate/social-media-analytics.mdx
index e2017905..225f1610 100644
--- a/src/content/docs/projects/intermediate/social-media-analytics.mdx
+++ b/src/content/docs/projects/intermediate/social-media-analytics.mdx
@@ -1,6 +1,6 @@
---
title: Social Media Analytics Dashboard
-description: Build a comprehensive social media analytics platform with real-time data fetching, sentiment analysis, engagement tracking, hashtag performance analysis, and AI-powered recommendations
+description: Build a multi-platform social analytics tool in Python — pull posts from Twitter/Facebook/Instagram (with a mock-data fallback), run TextBlob sentiment analysis, track engagement and hashtags, generate word clouds, and serve a Plotly Flask dashboard.
sidebar:
order: 64
hero:
@@ -13,276 +13,133 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+This project turns the firehose of social media into insight. It pulls posts from **Twitter, Facebook, and Instagram**, scores each one's sentiment with NLP, computes engagement rates, ranks hashtags, builds word clouds, and presents everything in a Plotly dashboard. The cleverest design choice: if you don't have API keys (and you probably don't), it transparently **falls back to realistic mock data** — so the whole thing runs and teaches on day one, then upgrades to live data when you add credentials. That graceful-degradation pattern is worth the price of admission alone.
-Create a comprehensive social media analytics platform that aggregates data from Twitter, Facebook, and Instagram, performs sentiment analysis, tracks engagement patterns, analyzes hashtag performance, and provides AI-powered recommendations for social media strategy optimization. Features both a modern web dashboard and command-line interface.
+You will leave understanding:
+- How to wrap three different platform APIs behind one normalized schema.
+- The **real-or-mock fallback** pattern that makes a project runnable without secrets.
+- Sentiment analysis with TextBlob — polarity scores and labels.
+- Extracting hashtags/mentions with regex and ranking them by engagement.
## Prerequisites
-
-- Python 3.7 or above
-- Text Editor or IDE
-- Strong understanding of Python syntax and OOP concepts
-- Knowledge of web development with Flask
-- Familiarity with data analysis and visualization
-- Understanding of social media APIs and authentication
-- Basic knowledge of machine learning and sentiment analysis
-- Experience with SQL databases and pandas
+- Python 3.7 or above.
+- A text editor or IDE.
+- `pip install flask textblob pandas plotly wordcloud matplotlib` (live mode also needs `tweepy`, `facebook-sdk`, `instaloader`).
+- `python -m textblob.download_corpora` for sentiment data.
+- API keys are **optional** — without them the app uses mock data.
## Getting Started
-### Create a new project
-1. Create a new project folder and name it `socialMediaAnalytics`.
-2. Create a new file and name it `socialmediaanalytics.py`.
-3. Install required dependencies: `pip install flask pandas matplotlib seaborn plotly tweepy facebook-sdk instaloader textblob wordcloud`
-4. Open the project folder in your favorite text editor or IDE.
-5. Copy the code below and paste it into your `socialmediaanalytics.py` file.
+### Create the project
+1. Create a folder named `social-analytics`.
+2. Inside it, create `socialmediaanalytics.py`.
+3. Install dependencies; download the TextBlob corpora.
### Write the code
-1. Add the following code to your `socialmediaanalytics.py` file.
-
-2. Save the file.
-3. Run the following command to start the application.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\socialMediaAnalytics> python socialmediaanalytics.py
-📱 Social Media Analytics Dashboard
-==================================================
-
-Choose interface:
-1. Web Dashboard (Recommended)
-2. Command Line Interface
-3. Generate Sample Data & Run Dashboard
-Enter choice (1-3): 3
+
-📊 Generating sample data...
-✅ Sample data generated and saved to database
-🚀 Starting web dashboard...
-📊 Access the dashboard at: http://localhost:5000
-🔗 Features available:
- - Real-time social media data fetching
- - Engagement and sentiment analytics
- - Hashtag performance analysis
- - AI-powered recommendations
- - Comprehensive reporting
+### Run it
+```cmd title="command"
+C:\Users\Your Name\social-analytics> python socialmediaanalytics.py
+# No API keys? It runs on mock data automatically.
+# Add keys in the config dicts to switch to live data.
```
-## Explanation
-
-1. The `SocialMediaAnalyticsAPI` class manages social media data collection and analysis.
-2. The `init_database()` method creates comprehensive tables for posts, analytics, hashtags, competitors, and campaigns.
-3. API integration supports Twitter, Facebook, and Instagram with proper authentication handling.
-4. Sentiment analysis uses TextBlob to analyze post content and classify emotions.
-5. Engagement pattern analysis tracks likes, shares, comments, and reach across platforms.
-6. Hashtag performance analysis identifies trending tags and their effectiveness.
-7. The `SocialMediaDashboard` class provides a Flask web interface with interactive charts.
-8. AI-powered recommendations engine suggests content strategies based on performance data.
-9. Real-time data fetching allows continuous monitoring and updates.
-10. Comprehensive reporting generates detailed analytics with export functionality.
-11. The system supports both web dashboard and command-line interfaces.
-12. Advanced features include competitor analysis, campaign tracking, and content insights.
-
-## Next Steps
-
-Congratulations! You have successfully created a Social Media Analytics Dashboard in Python. Experiment with the code and see if you can modify the application. Here are a few suggestions:
-- Add LinkedIn and TikTok API integration for complete coverage
-- Implement machine learning models for engagement prediction
-- Create automated posting and scheduling features
-- Add real-time monitoring with alert notifications
-- Implement advanced NLP for topic modeling and trend detection
-- Create collaborative features for social media teams
-- Add A/B testing capabilities for content optimization
-- Implement brand mention tracking and crisis management
+## Step-by-Step Explanation
-## Conclusion
-
-In this project, you learned how to create a Social Media Analytics Dashboard in Python. You also learned about API integration, data analysis, sentiment analysis, web dashboard development, and building comprehensive social media monitoring systems. You can find the source code on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/socialmediaanalytics.py)
-
-## Code Explanation
-
-### Database Schema Design
-```python title="socialmediaanalytics.py" showLineNumbers{1}
-def init_database(self):
- cursor.execute('''
- CREATE TABLE IF NOT EXISTS posts (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- platform TEXT NOT NULL,
- post_id TEXT UNIQUE NOT NULL,
- content TEXT,
- author TEXT,
- timestamp DATETIME,
- likes INTEGER DEFAULT 0,
- shares INTEGER DEFAULT 0,
- comments INTEGER DEFAULT 0,
- engagement_rate REAL DEFAULT 0,
- sentiment_score REAL DEFAULT 0,
- sentiment_label TEXT,
- hashtags TEXT,
- mentions TEXT
- )
- ''')
-```
+### 1. One schema for three platforms
+Twitter, Facebook, and Instagram return wildly different JSON, but the `posts` table normalizes them all to the same columns — `platform`, `content`, `likes`, `shares`, `comments`, `sentiment_score`, `hashtags`, … Normalizing to a common shape on ingest is what lets you compare platforms later. Extra tables (`analytics`, `hashtags`, `competitors`, `campaigns`) round out the warehouse.
-Creates comprehensive database schema for multi-platform social media data with engagement metrics.
-
-### Social Media API Integration
-```python title="socialmediaanalytics.py" showLineNumbers{1}
+### 2. The real-or-mock fallback (the key pattern)
+```python title="socialmediaanalytics.py"
def fetch_twitter_data(self, username, count=100):
- tweets = tweepy.Cursor(
- self.twitter_api.user_timeline,
- screen_name=username,
- include_rts=False,
- tweet_mode='extended'
- ).items(count)
-
- for tweet in tweets:
- sentiment = TextBlob(tweet.full_text).sentiment
- hashtags = [tag['text'] for tag in tweet.entities['hashtags']]
-
- post_data = {
- 'platform': 'twitter',
- 'content': tweet.full_text,
- 'likes': tweet.favorite_count,
- 'shares': tweet.retweet_count,
- 'sentiment_score': sentiment.polarity,
- 'hashtags': json.dumps(hashtags)
- }
+ if not self.twitter_api:
+ return self.generate_mock_twitter_data(username, count) # no keys → mock
+ try:
+ tweets = tweepy.Cursor(self.twitter_api.user_timeline, ...).items(count)
+ ...
+ except Exception as e:
+ print(f"Twitter API error: {e}")
+ return self.generate_mock_twitter_data(username, count) # error → mock
```
-
-Integrates with Twitter API for real-time data collection with automatic sentiment analysis.
-
-### Sentiment Analysis Engine
-```python title="socialmediaanalytics.py" showLineNumbers{1}
-def analyze_sentiment_trends(self, platform=None, days=30):
- query = '''
- SELECT
- platform,
- DATE(timestamp) as date,
- sentiment_label,
- COUNT(*) as count,
- AVG(sentiment_score) as avg_sentiment
- FROM posts
- WHERE timestamp >= date('now', '-{} days')
- GROUP BY platform, DATE(timestamp), sentiment_label
- '''.format(days)
-
- df = pd.read_sql_query(query, conn)
- return df
+Two safety nets: no credentials *or* an API failure both fall back to generated data. The app is **never** broken by a missing key or a rate limit. This is how you ship a demo that anyone can run while keeping a real integration one config edit away.
+
+### 3. Sentiment analysis with TextBlob
+```python title="socialmediaanalytics.py"
+sentiment = TextBlob(content).sentiment # .polarity in [-1, 1]
+sentiment_label = ('positive' if sentiment.polarity > 0.1
+ else 'negative' if sentiment.polarity < -0.1
+ else 'neutral')
```
+`TextBlob` gives a **polarity** from -1 (negative) to +1 (positive). The ±0.1 dead zone around zero is deliberate — text near neutral shouldn't be forced into positive/negative. One line of NLP turns raw captions into a measurable mood signal.
-Analyzes sentiment trends over time with platform-specific insights and scoring.
-
-### Engagement Pattern Analysis
-```python title="socialmediaanalytics.py" showLineNumbers{1}
-def analyze_engagement_patterns(self, platform=None, days=30):
- query = '''
- SELECT
- platform,
- DATE(timestamp) as date,
- AVG(engagement_rate) as avg_engagement,
- SUM(likes) as total_likes,
- SUM(shares) as total_shares,
- SUM(comments) as total_comments,
- COUNT(*) as post_count
- FROM posts
- WHERE timestamp >= date('now', '-{} days')
- GROUP BY platform, DATE(timestamp)
- '''.format(days)
+### 4. Extracting hashtags and mentions
+```python title="socialmediaanalytics.py"
+hashtags = re.findall(r'#\w+', message) # every #tag
```
+For platforms that don't hand you structured entities, a regex over the text pulls hashtags (`#\w+`). Twitter's API provides them structured (`tweet.entities['hashtags']`), so the code uses whichever is available — another normalization step.
-Tracks engagement patterns across platforms with comprehensive metrics and trends.
-
-### Hashtag Performance Analytics
-```python title="socialmediaanalytics.py" showLineNumbers{1}
-def analyze_hashtag_performance(self, platform=None, limit=20):
- for hashtags_json, engagement, likes, shares, comments in results:
- hashtags = json.loads(hashtags_json)
- for hashtag in hashtags:
- hashtag_stats[hashtag]['usage_count'] += 1
- hashtag_stats[hashtag]['total_engagement'] += engagement
- hashtag_stats[hashtag]['avg_engagement'] = stats['total_engagement'] / stats['usage_count']
-
- sorted_hashtags = sorted(hashtag_stats.items(),
- key=lambda x: (x[1]['avg_engagement'], x[1]['usage_count']),
- reverse=True)
+### 5. Storing as JSON in a column
+```python title="socialmediaanalytics.py"
+'hashtags': json.dumps(hashtags),
+'mentions': json.dumps(mentions)
```
+Lists don't fit in a SQL cell, so they're serialized to JSON strings. Pragmatic for a small app (deserialize with `json.loads` on read); at scale you'd use a separate hashtags table with a join.
-Analyzes hashtag effectiveness with usage frequency and engagement correlation.
+### 6. Engagement, word clouds, and the dashboard
+Engagement rate combines likes/shares/comments against reach. A `WordCloud` visualizes dominant terms. The Flask app exposes each analysis as a JSON endpoint and renders interactive Plotly charts client-side — Python computes, the browser draws.
-### AI Recommendations Engine
-```python title="socialmediaanalytics.py" showLineNumbers{1}
-def generate_recommendations(self):
- recommendations = {
- 'content_strategy': [],
- 'posting_schedule': [],
- 'hashtag_strategy': [],
- 'engagement_tactics': []
- }
-
- if insights.get('avg_engagement_by_sentiment'):
- best_sentiment = max(insights['avg_engagement_by_sentiment'].items(), key=lambda x: x[1])
- recommendations['content_strategy'].append(
- f"Focus on {best_sentiment[0]} content - it has {best_sentiment[1]:.1f}% higher engagement"
- )
-```
+## A Note on Live-API Reality
-Generates AI-powered recommendations based on performance data analysis and trends.
+The mock mode is honest about something tutorials gloss over: **these APIs are hard to use for real now.**
-### Interactive Web Dashboard
-```python title="socialmediaanalytics.py" showLineNumbers{1}
-@self.app.route('/api/engagement-chart')
-def api_engagement_chart():
- df = self.analytics_api.analyze_engagement_patterns(days=30)
-
- fig = px.line(df, x='date', y='avg_engagement', color='platform',
- title='Engagement Rate Over Time')
-
- graphJSON = json.dumps(fig, cls=PlotlyJSONEncoder)
- return graphJSON
-```
+| Platform | Reality in 2024+ |
+|---|---|
+| Twitter/X | `user_timeline` is largely paywalled; `tweepy` v4 uses a different client (`tweepy.Client`) |
+| Instagram | `instaloader` scraping risks rate-limits/bans; no official read API for this |
+| Facebook | Requires app review and page tokens with specific permissions |
-Creates interactive web dashboard with real-time charts using Plotly and Flask.
+The mock fallback isn't just a convenience — it's what keeps this project *teachable* despite platform lockdown. Treat live integration as an advanced, optional extension.
-## Features
+## Common Mistakes
-- **Multi-Platform Integration**: Support for Twitter, Facebook, and Instagram APIs
-- **Real-Time Data Fetching**: Continuous monitoring and data collection
-- **Sentiment Analysis**: Advanced emotion detection using natural language processing
-- **Engagement Tracking**: Comprehensive metrics including likes, shares, comments, and reach
-- **Hashtag Analytics**: Performance analysis and trending hashtag identification
-- **Interactive Dashboard**: Modern web interface with real-time charts and visualizations
-- **AI Recommendations**: Machine learning-powered strategy suggestions
-- **Competitor Analysis**: Track and compare competitor performance
-- **Campaign Tracking**: Monitor social media campaign effectiveness
-- **Comprehensive Reporting**: Detailed analytics with export functionality
+| Problem | Cause | Fix |
+|---|---|---|
+| `Missing corpora` from TextBlob | NLTK data not downloaded | `python -m textblob.download_corpora` |
+| Live Twitter calls 401/403 | v1.1 endpoints deprecated/paywalled | Use `tweepy.Client` (v2) or stay on mock |
+| Instagram login fails/bans | Scraping against ToS + rate limits | Prefer mock; don't automate logins |
+| All sentiment looks "neutral" | Short/emoji-only text | Expected; TextBlob is lexical, not deep |
+| `UNIQUE constraint` on post_id | Re-ingesting same posts | `INSERT OR IGNORE`, or upsert |
+| Word cloud import error | `wordcloud` needs build tools | Install a prebuilt wheel |
-## Next Steps
+## Variations to Try
-### Enhancements
-- Add LinkedIn and TikTok API integration for complete coverage
-- Implement machine learning models for engagement prediction
-- Create automated posting and scheduling features
-- Add real-time monitoring with alert notifications
-- Implement advanced NLP for topic modeling and trend detection
-- Create collaborative features for social media teams
-- Add A/B testing capabilities for content optimization
-- Implement brand mention tracking and crisis management
+1. **Run on mock first** — explore every feature with zero keys.
+2. **Upgrade to `tweepy.Client`** — the supported v2 Twitter API.
+3. **Better sentiment** — swap TextBlob for VADER (great on social text) or a transformer.
+4. **Emoji sentiment** — emojis carry mood TextBlob ignores.
+5. **Competitor tracking** — the `competitors` table is ready to use.
+6. **Posting-time analysis** — find when engagement peaks.
+7. **Scheduled collection** — the `schedule` import enables periodic pulls.
+8. **Export reports** — weekly PDF summaries per platform.
-### Learning Extensions
-- Study advanced natural language processing techniques
-- Explore machine learning for social media prediction
-- Learn about real-time data streaming and processing
-- Practice with advanced data visualization libraries
-- Understand social media marketing strategies and best practices
-- Explore cloud deployment for scalable analytics platforms
+## Real-World Applications
+- **Social media management** — Hootsuite/Buffer-style analytics.
+- **Brand monitoring** — sentiment and mention tracking.
+- **Influencer analysis** — engagement-rate vetting.
+- **Campaign measurement** — reach, conversions, ROI.
## Educational Value
+- **API integration** — multiple providers behind one interface.
+- **Resilient design** — the real-or-mock fallback pattern.
+- **NLP basics** — sentiment polarity and text feature extraction.
+- **Data viz** — word clouds and interactive Plotly dashboards.
-This project teaches:
-- **API Integration**: Working with multiple social media APIs and authentication
-- **Data Analysis**: Advanced analytics with pandas and statistical analysis
-- **Sentiment Analysis**: Natural language processing and emotion detection
-- **Web Development**: Creating interactive dashboards with Flask and Plotly
-- **Database Design**: Designing schemas for complex social media data
-- **Machine Learning**: Implementing recommendation systems and predictive analytics
-- **Data Visualization**: Creating compelling charts and interactive dashboards
-- **Social Media Strategy**: Understanding engagement metrics and optimization techniques
+## Next Steps
+- Explore everything in **mock mode**, then add one real API.
+- Replace TextBlob with **VADER** for social-tuned sentiment.
+- Enable **scheduled collection** and **competitor tracking**.
+- Add **best-time-to-post** analysis and **report export**.
-Perfect for understanding social media analytics, API integration, and building production-ready analytics platforms for digital marketing and social media management.
+## Conclusion
+You built a multi-platform social analytics tool that ingests posts, scores sentiment, ranks hashtags, and visualizes it all — and, crucially, runs with or without API keys thanks to a clean real-or-mock fallback. That resilience pattern, plus normalizing three messy APIs into one schema, is exactly how production integrations are built to survive missing credentials and flaky upstreams. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/socialmediaanalytics.py). Explore more data and NLP projects on Python Central Hub.
diff --git a/src/content/docs/projects/intermediate/url-expander.mdx b/src/content/docs/projects/intermediate/url-expander.mdx
index 77edd4a5..1719d841 100644
--- a/src/content/docs/projects/intermediate/url-expander.mdx
+++ b/src/content/docs/projects/intermediate/url-expander.mdx
@@ -1,5 +1,6 @@
---
title: URL Expander
+description: Build a Tkinter URL expander in Python that follows HTTP redirects to reveal where a shortened link really points — then add a full redirect chain, safety checks, timeouts, batch processing, and clipboard integration.
sidebar:
order: 105
hero:
@@ -12,56 +13,167 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+Shortened links (`bit.ly`, `t.co`, `tinyurl`) hide their real destination — convenient for sharing, risky for clicking. A URL expander follows the chain of HTTP redirects and shows you where a short link *actually* lands before you visit it. In this tutorial you build a small Tkinter app around a single `requests` call, then turn it into a genuinely useful safety tool: it reveals the **full** redirect chain, flags suspicious hops, handles timeouts, expands links in bulk, and copies results to the clipboard.
-Build a URL Expander application that expands shortened URLs to their original form. The app provides a GUI for entering URLs and displaying the expanded result. This project demonstrates HTTP requests, URL handling, and GUI development in Python.
+You will leave understanding:
+- How HTTP redirects (301/302) work and how `requests` follows them.
+- The difference between a `HEAD` and a `GET` request, and why `HEAD` is cheaper here.
+- How to handle network errors without crashing the UI.
+- Why expanding a link is a security practice, not just a convenience.
## Prerequisites
-- Python 3.6 or above
-- Text Editor or IDE
-- Basic understanding of Python syntax
-- Familiarity with Tkinter for GUI development
-- Knowledge of HTTP requests
+- Python 3.6 or above.
+- A text editor or IDE.
+- The `requests` library: `pip install requests`.
+- Tkinter (bundled with Python).
+- A basic grasp of HTTP requests and responses.
## Getting Started
-### Creating a new project
-1. Create a new project folder and name it `url_expander`.
-2. Create a new file inside the folder and name it `url_expander.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into the `url_expander.py` file.
+
+### Create the project
+1. Create a folder named `url-expander`.
+2. Inside it, create `url_expander.py`.
+3. Install the dependency: `pip install requests`.
### Write the code
-## Key Features
-- Accept shortened URLs as input
-- Display expanded URLs
-- GUI interface for user interaction
+### Run it
+```cmd title="command"
+C:\Users\Your Name\url-expander> python url_expander.py
+# Paste a short URL (e.g. https://bit.ly/xyz) and click "Expand URL".
+# A dialog shows the final destination.
+```
-## Explanation
+## Step-by-Step Explanation
-### Expanding URLs
-The app uses the `requests` library to follow redirects and expand URLs:
-```python title="url_expander.py" showLineNumbers={1}
-response = requests.head(short_url, allow_redirects=True)
-return response.url
+### 1. The core: follow the redirects
+```python title="url_expander.py"
+def expand_url(short_url):
+ try:
+ response = requests.head(short_url, allow_redirects=True)
+ return response.url
+ except requests.RequestException as e:
+ return str(e)
```
+Two ideas do all the work:
+- **`requests.head`** asks only for the response *headers*, not the page body. Since we only care about the final URL, downloading the whole page would be wasteful.
+- **`allow_redirects=True`** tells `requests` to chase every `Location` header automatically. After the last hop, `response.url` holds the real destination.
-### Displaying Results
-Expanded URLs are shown in a Tkinter message box:
-```python title="url_expander.py" showLineNumbers={1}
+Catching `requests.RequestException` (the base class for all `requests` errors) means a dead link or DNS failure returns a message instead of crashing.
+
+### 2. Input guard
+```python title="url_expander.py"
+short_url = self.url_entry.get()
+if not short_url:
+ messagebox.showerror("Error", "Please enter a URL.")
+ return
+```
+Never trust an empty field. Bail early with a clear message.
+
+### 3. Show the result
+```python title="url_expander.py"
+expanded_url = expand_url(short_url)
messagebox.showinfo("Expanded URL", f"Original URL: {expanded_url}")
```
+A popup is fine for one URL. For more, you'll want a results panel (below).
+
+## Upgrade: Show the Full Redirect Chain
+
+A single destination hides intermediate hops — and those hops are where shady redirectors live. `response.history` records every step:
+```python title="chain.py"
+import requests
-## Running the Application
-1. Save the file.
-2. Install required dependencies:
-```bash
-pip install requests
+def redirect_chain(short_url, timeout=10):
+ r = requests.get(short_url, allow_redirects=True, timeout=timeout)
+ hops = [resp.url for resp in r.history] + [r.url]
+ return hops
+
+for i, url in enumerate(redirect_chain("https://bit.ly/example")):
+ print(f"{i}: {url}")
```
-3. Run the application:
-```bash
-python url_expander.py
+Seeing `bit.ly → tracking.example → final.com` tells you far more than `final.com` alone.
+
+## Add a Timeout (Always)
+
+A request with no timeout can hang forever, freezing your app:
+```python title="timeout.py"
+requests.head(short_url, allow_redirects=True, timeout=10)
```
+**Rule of thumb:** every network call in production code gets a `timeout`. Catch `requests.Timeout` separately to tell the user "the server is slow" rather than "the URL is broken".
+
+## Flag Suspicious Links
+
+A lightweight safety check before the user clicks:
+```python title="safety.py"
+from urllib.parse import urlparse
+
+SUSPICIOUS_TLDS = {".zip", ".mov", ".tk", ".xyz"}
+
+def looks_risky(url):
+ host = urlparse(url).netloc.lower()
+ flags = []
+ if any(url.lower().endswith(t) for t in SUSPICIOUS_TLDS):
+ flags.append("unusual TLD")
+ if url.count("//") > 1:
+ flags.append("nested redirect")
+ if "@" in url:
+ flags.append("credentials in URL")
+ return flags
+```
+This is heuristic, not a malware scanner — but "credentials in URL" and "nested redirect" catch a surprising number of phishing tricks.
+
+## Batch Mode
+
+Expand a whole list at once and show results in a `Text` widget instead of popups:
+```python title="batch.py"
+def expand_many(self):
+ urls = self.input_text.get("1.0", "end").splitlines()
+ self.output_text.delete("1.0", "end")
+ for url in filter(str.strip, urls):
+ final = expand_url(url.strip())
+ self.output_text.insert("end", f"{url.strip()} -> {final}\n")
+```
+Now you can paste 50 links and audit them in one click.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| App hangs on a bad link | No `timeout` set | Pass `timeout=10` to every request |
+| Some shorteners don't expand | They block `HEAD` requests | Fall back to `requests.get` |
+| Only the final URL shown | Ignored `response.history` | Build the chain from `history + [r.url]` |
+| `SSLError` on valid sites | Outdated certificates | Update `certifi`; never disable verification in production |
+| UI freezes during batch | Network calls on the main thread | Run expansion in a background thread |
+| Crash on malformed input | No URL validation | Check scheme with `urlparse` before requesting |
+
+## Variations to Try
+
+1. **Clipboard integration** — auto-read the clipboard and expand on launch.
+2. **Browser extension companion** — expose the logic as a small Flask API.
+3. **QR support** — decode a QR image to a short URL, then expand it.
+4. **History log** — save every expansion to a JSON file with a timestamp.
+5. **Threaded UI** — keep the window responsive during slow lookups.
+6. **Reputation lookup** — query a URL-reputation API and show a verdict.
+7. **CLI mode** — accept a URL as a command-line argument for scripting.
+
+## Real-World Applications
+- **Security & anti-phishing** — verify links before clicking in email or chat.
+- **Social media tooling** — audit campaign links and tracking parameters.
+- **Link analytics** — uncover the tracking domains a redirect passes through.
+- **Content moderation** — inspect user-submitted short links at scale.
+
+## Educational Value
+- **HTTP fundamentals** — redirects, status codes, `HEAD` vs. `GET`.
+- **Robust networking** — timeouts, exception hierarchies, retries.
+- **Security thinking** — why obscured destinations are a risk.
+- **Responsive GUIs** — moving slow work off the main thread.
+
+## Next Steps
+- Print the **full redirect chain** instead of just the destination.
+- Add a **timeout** and `HEAD`→`GET` fallback.
+- Implement the **suspicious-link** heuristics.
+- Build **batch mode** with a results panel and a saved history log.
## Conclusion
-This URL Expander project is a great way to learn about HTTP requests and URL handling in Python. You can extend it by adding batch expansion, URL validation, or analytics features.
+You built a URL expander from a single `requests.head` call and grew it into a redirect auditor that reveals every hop, flags risky links, survives slow servers, and processes links in bulk. Underneath a tiny UI sits a genuinely useful security habit: know where a link goes before you go there. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/url_expander.py). Find more networking projects on Python Central Hub.
diff --git a/src/content/docs/projects/intermediate/url-shortener-analytics.mdx b/src/content/docs/projects/intermediate/url-shortener-analytics.mdx
index cfd78503..36e47dea 100644
--- a/src/content/docs/projects/intermediate/url-shortener-analytics.mdx
+++ b/src/content/docs/projects/intermediate/url-shortener-analytics.mdx
@@ -1,6 +1,6 @@
---
title: URL Shortener with Analytics
-description: Build a comprehensive URL shortening service with click tracking, analytics dashboard, QR code generation, and web interface
+description: Build a Bitly-style URL shortener in Python with Flask and SQLite — short codes, custom aliases, QR codes, expiry, and a full click-analytics dashboard (devices, browsers, referrers, daily charts) — plus a REST API and CLI.
sidebar:
order: 62
hero:
@@ -13,238 +13,159 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+This is Bitly in miniature — and it's a brilliant teaching project because a "simple" URL shortener quietly touches code generation, redirects, a database, QR codes, request inspection, data aggregation, and charting. You'll build a service that turns long URLs into short codes (or custom aliases), redirects visitors while **logging every click**, and renders an analytics dashboard with device/browser/referrer breakdowns and a daily-clicks chart. It ships three interfaces over one engine: a web UI, a REST API, and a CLI.
-Create a comprehensive URL shortening service with advanced analytics capabilities including click tracking, device/browser statistics, QR code generation, and a web dashboard. This project demonstrates database design, web development, data visualization, and analytics implementation.
+You will leave understanding:
+- How short codes are generated, kept unique, and resolved back to the original URL.
+- The redirect-and-track pattern that powers all link analytics.
+- How to mine the HTTP request (user-agent, referrer, IP) for analytics.
+- How `GROUP BY` turns a raw click log into dashboards and charts.
## Prerequisites
-
-- Python 3.7 or above
-- Text Editor or IDE
-- Solid understanding of Python syntax and OOP concepts
-- Knowledge of web development concepts (HTML, HTTP)
-- Familiarity with databases and SQL operations
-- Understanding of data visualization principles
-- Basic knowledge of Flask web framework
+- Python 3.7 or above.
+- A text editor or IDE.
+- `pip install flask qrcode pillow matplotlib`.
+- SQL basics (`INSERT`, `SELECT`, `GROUP BY`) and Flask routing.
+- Familiarity with the [QR Code Attendance System](./qr-code-attendance-system) helps for the QR part.
## Getting Started
-### Create a new project
-1. Create a new project folder and name it `urlShortenerAnalytics`.
-2. Create a new file and name it `urlshorteneranalytics.py`.
-3. Install required dependencies: `pip install flask matplotlib qrcode pillow`
-4. Open the project folder in your favorite text editor or IDE.
-5. Copy the code below and paste it into your `urlshorteneranalytics.py` file.
+### Create the project
+1. Create a folder named `url-shortener`.
+2. Inside it, create `urlshorteneranalytics.py`.
+3. Install dependencies: `pip install flask qrcode pillow matplotlib`.
### Write the code
-1. Add the following code to your `urlshorteneranalytics.py` file.
-
-2. Save the file.
-3. Run the following command to start the application.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\urlShortenerAnalytics> python urlshorteneranalytics.py
-🔗 URL Shortener with Analytics
-========================================
-
-📋 Menu:
-1. Create Short URL
-2. View Analytics
-3. List All URLs
-4. Start Web Server
-5. Delete URL
-6. Exit
-
-Enter your choice (1-6): 1
-Enter the URL to shorten: https://github.com/user/repo
-Custom alias (optional): my-repo
-Title (optional): My GitHub Repository
-Description (optional): My awesome Python project
-
-✅ Short URL created!
-Short URL: http://localhost:5000/my-repo
-QR Code generated: Yes
-
-Enter your choice (1-6): 4
-🌐 Starting web server...
-Access the web interface at: http://localhost:5000
-```
-
-## Explanation
-
-1. The `sqlite3` module provides database functionality for storing URLs and analytics data.
-2. The `URLShortenerAnalytics` class manages the core functionality and database operations.
-3. The `init_database()` method creates tables for URLs and click tracking analytics.
-4. The `generate_short_code()` function creates unique random codes for shortened URLs.
-5. The `create_short_url()` method handles URL creation with optional custom aliases and expiration.
-6. The `track_click()` function records detailed analytics for each URL access.
-7. The `generate_qr_code()` method creates QR codes for easy mobile sharing.
-8. The `get_analytics()` function provides comprehensive statistics and visualizations.
-9. The Flask web interface offers a user-friendly dashboard for managing URLs.
-10. The `generate_analytics_chart()` method creates visual charts using matplotlib.
-11. Device and browser parsing extracts information from user agent strings.
-12. The web templates provide responsive HTML interfaces for all features.
-
-## Next Steps
-
-Congratulations! You have successfully created a URL Shortener with Analytics in Python. Experiment with the code and see if you can modify the application. Here are a few suggestions:
-- Add user authentication and personal dashboards
-- Implement bulk URL shortening from CSV files
-- Create mobile app integration with API endpoints
-- Add social media sharing buttons and tracking
-- Implement A/B testing for different short URLs
-- Add geographic location tracking for clicks
-- Create scheduled reports and email notifications
-- Implement URL preview and safety checking
-- Add collaborative features for teams
+
-## Conclusion
-
-In this project, you learned how to create a URL Shortener with Analytics in Python. You also learned about database design, web development, data visualization, and building comprehensive analytics systems. You can find the source code on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/urlshorteneranalytics.py)
-
-## Code Explanation
-
-### Database Schema Design
-```python title="urlshorteneranalytics.py" showLineNumbers{1}
-def init_database(self):
- cursor.execute('''
- CREATE TABLE IF NOT EXISTS urls (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- original_url TEXT NOT NULL,
- short_code TEXT UNIQUE NOT NULL,
- custom_alias TEXT UNIQUE,
- title TEXT,
- description TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- expires_at TIMESTAMP,
- is_active BOOLEAN DEFAULT 1,
- total_clicks INTEGER DEFAULT 0,
- unique_clicks INTEGER DEFAULT 0
- )
- ''')
+### Run it
+```cmd title="command"
+C:\Users\Your Name\url-shortener> python urlshorteneranalytics.py
+# Menu appears. Choose 4 to start the web server, then open http://localhost:5000
```
-Creates comprehensive database schema for URLs with metadata and analytics tracking.
+## Step-by-Step Explanation
-### Short Code Generation
-```python title="urlshorteneranalytics.py" showLineNumbers{1}
+### 1. Generating a unique short code
+```python title="urlshorteneranalytics.py"
def generate_short_code(self, length=6):
characters = string.ascii_letters + string.digits
while True:
short_code = ''.join(random.choice(characters) for _ in range(length))
- if not self.get_url_by_code(short_code):
+ if not self.get_url_by_code(short_code): # collision check
return short_code
```
+Six characters from a 62-symbol alphabet gives 62⁶ ≈ 56 billion combinations. The `while True` loop regenerates on the rare collision and only returns a code that isn't already taken — the `UNIQUE` constraint on the column is the safety net.
-Generates unique short codes with collision detection and validation.
-
-### Click Analytics Tracking
-```python title="urlshorteneranalytics.py" showLineNumbers{1}
-def track_click(self, url_id, ip_address=None, user_agent=None, referrer=None):
- device_type = self.parse_device_type(user_agent)
- browser = self.parse_browser(user_agent)
-
- cursor.execute('''
- INSERT INTO clicks (url_id, ip_address, user_agent, referrer, device_type, browser)
- VALUES (?, ?, ?, ?, ?, ?)
- ''', (url_id, ip_address, user_agent, referrer, device_type, browser))
+### 2. Validating the input URL
+```python title="urlshorteneranalytics.py"
+def validate_url(self, url):
+ result = urlparse(url)
+ return all([result.scheme, result.netloc]) # needs http(s):// and a host
```
+A real URL needs both a scheme and a network location. Rejecting junk up front keeps the database clean and the redirects working.
-Records detailed click analytics including device type, browser, and referrer information.
-
-### QR Code Generation
-```python title="urlshorteneranalytics.py" showLineNumbers{1}
-def generate_qr_code(self, url):
- qr = qrcode.QRCode(version=1, box_size=10, border=5)
- qr.add_data(url)
- qr.make(fit=True)
-
- img = qr.make_image(fill_color="black", back_color="white")
- img_str = base64.b64encode(buffer.getvalue()).decode()
- return f"data:image/png;base64,{img_str}"
-```
-
-Creates QR codes for URLs and converts them to base64 for web display.
-
-### Analytics Dashboard
-```python title="urlshorteneranalytics.py" showLineNumbers{1}
-def get_analytics(self, url_id):
- # Daily clicks over time
- cursor.execute('''
- SELECT DATE(clicked_at) as date, COUNT(*) as clicks
- FROM clicks WHERE url_id = ?
- GROUP BY DATE(clicked_at) ORDER BY date
- ''', (url_id,))
-
- # Device and browser statistics
- cursor.execute('''
- SELECT device_type, COUNT(*) as count
- FROM clicks WHERE url_id = ? GROUP BY device_type
- ''', (url_id,))
-```
+### 3. Two tables: links and clicks
+The schema splits **what** (the `urls` table) from **events** (the `clicks` table). Every redirect writes a row to `clicks` with timestamp, IP, user-agent, referrer, device, and browser. This event-log design is the foundation of all analytics — store raw events, aggregate later.
-Provides comprehensive analytics including time series data and demographic breakdowns.
-
-### Web Interface Integration
-```python title="urlshorteneranalytics.py" showLineNumbers{1}
+### 4. The redirect that tracks
+```python title="urlshorteneranalytics.py"
@app.route('/')
def redirect_url(short_code):
url_data = shortener.get_url_by_code(short_code)
-
- # Track the click
- shortener.track_click(
- url_id=url_data[0],
- ip_address=request.remote_addr,
- user_agent=request.headers.get('User-Agent'),
- referrer=request.headers.get('Referer')
- )
-
- return redirect(url_data[1])
+ if not url_data:
+ return "URL not found", 404
+ if url_data[7] and datetime.datetime.now() > datetime.datetime.fromisoformat(url_data[7]):
+ return "URL has expired", 410 # 410 Gone for expired links
+ shortener.track_click(url_id=url_data[0],
+ ip_address=request.remote_addr,
+ user_agent=request.headers.get('User-Agent'),
+ referrer=request.headers.get('Referer'))
+ return redirect(url_data[1]) # send the visitor on their way
```
+This is the heart of the app: look up the code, check expiry (note the correct `410 Gone` status), **log the click**, then redirect. The visitor barely notices the detour.
+
+### 5. Mining the request for analytics
+```python title="urlshorteneranalytics.py"
+def parse_device_type(self, user_agent):
+ ua = user_agent.lower()
+ if 'mobile' in ua or 'android' in ua or 'iphone' in ua: return "Mobile"
+ elif 'tablet' in ua or 'ipad' in ua: return "Tablet"
+ else: return "Desktop"
+```
+The `User-Agent` header and `Referer` are goldmines: parse them into device type and browser, and you can show *who* clicked and *where from* — with no extra tracking.
-Handles URL redirection while automatically tracking analytics data.
-
-## Features
+### 6. Aggregating with `GROUP BY`
+```python title="urlshorteneranalytics.py"
+SELECT DATE(clicked_at) as date, COUNT(*) as clicks
+FROM clicks WHERE url_id = ?
+GROUP BY DATE(clicked_at) ORDER BY date
+```
+The dashboard is just `GROUP BY` queries: clicks per day, per device, per browser, per referrer. Raw events in, summarized insight out — the universal analytics pattern.
-- **URL Shortening**: Create short, memorable URLs with optional custom aliases
-- **QR Code Generation**: Automatic QR code creation for mobile sharing
-- **Click Analytics**: Detailed tracking of clicks, devices, browsers, and referrers
-- **Expiration Dates**: Set automatic expiration for temporary links
-- **Web Dashboard**: Complete web interface for managing URLs and viewing analytics
-- **Data Visualization**: Charts and graphs for analytics insights
-- **REST API**: Programmatic access to all features via JSON API
-- **Device Detection**: Automatic detection of mobile, tablet, and desktop users
-- **Browser Analytics**: Track which browsers are used to access your links
-- **Referrer Tracking**: See where your traffic is coming from
+### 7. QR codes and charts as data URIs
+```python title="urlshorteneranalytics.py"
+img_str = base64.b64encode(buffer.getvalue()).decode()
+return f"data:image/png;base64,{img_str}"
+```
+Both the QR code and the matplotlib charts are rendered in memory and base64-encoded into `data:` URIs, so they embed directly in the HTML — no temp files to manage.
-## Next Steps
+## Caveat: Unique Clicks by IP
-### Enhancements
-- Add user authentication and personal dashboards
-- Implement bulk URL shortening from CSV files
-- Create mobile app integration with API endpoints
-- Add social media sharing buttons and tracking
-- Implement A/B testing for different short URLs
-- Add geographic location tracking for clicks
-- Create scheduled reports and email notifications
-- Implement URL preview and safety checking
-
-### Learning Extensions
-- Study web analytics and tracking technologies
-- Explore advanced data visualization techniques
-- Learn about user experience optimization
-- Practice with cloud deployment and scaling
-- Understand security best practices for URL services
-- Explore machine learning for click prediction
+```python title="urlshorteneranalytics.py"
+SELECT COUNT(DISTINCT ip_address) FROM clicks WHERE url_id = ?
+```
+"Unique clicks" here counts distinct IPs. It's a reasonable approximation but imperfect: people behind the same office NAT share an IP (undercount), and mobile users roam across IPs (overcount). Real analytics use a cookie or fingerprint. Know the limitation when you quote the number.
+
+## Production Notes
+
+| Issue | Fix |
+|---|---|
+| `debug=True, host='0.0.0.0'` | `debug=False`; bind deliberately |
+| Recomputing `unique_clicks` on every click | Expensive at scale — precompute or cache |
+| Charts rendered per request with matplotlib | Cache, or move to client-side JS charts |
+| No auth on delete/analytics | Tie links to accounts; check ownership |
+| `random` short codes | Fine; for guaranteed-unguessable use `secrets` |
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| Short code collisions | Not checking existing codes | Loop until unique (the code does) |
+| Expired links still redirect | Forgot the expiry check | Compare `expires_at` before redirecting; return `410` |
+| Every visitor counts as "unique" | No IP/cookie dedup | `COUNT(DISTINCT ip_address)` or cookies |
+| `matplotlib` errors on a server | No display backend | Use the `Agg` backend (`matplotlib.use('Agg')`) |
+| Redirect loops | Shortened a URL pointing at your own domain | Reject self-referential URLs |
+| Slow dashboard | Re-aggregating everything each load | Index `clicks.url_id`; cache results |
+
+## Variations to Try
+
+1. **Accounts & ownership** — users manage only their own links.
+2. **Cookie-based unique visitors** — more accurate than IP.
+3. **Geo-analytics** — map clicks by country via an IP-geolocation API.
+4. **Password-protected links** — the schema already has `password_hash`.
+5. **Bulk shortening** — upload a CSV of URLs.
+6. **Client-side charts** — swap matplotlib for Chart.js.
+7. **Link editing** — change the destination without changing the code.
+8. **Rate limiting** — stop abuse of the shorten endpoint.
+
+## Real-World Applications
+- **Marketing** — trackable campaign links (UTM-style).
+- **Social media** — fit long URLs into character limits.
+- **Print/QR** — short codes behind posters and packaging.
+- **Internal analytics** — measure which docs/links get traffic.
## Educational Value
+- **Code generation** — uniqueness, alphabets, collision handling.
+- **Event-log analytics** — store raw, aggregate with `GROUP BY`.
+- **HTTP introspection** — user-agent and referrer parsing.
+- **Multi-interface design** — one engine behind web, API, and CLI.
-This project teaches:
-- **Database Design**: Creating normalized schemas for analytics data
-- **Web Development**: Building full-stack applications with Flask
-- **Data Analytics**: Implementing comprehensive tracking and reporting
-- **QR Codes**: Generating and integrating QR codes for mobile experiences
-- **User Agent Parsing**: Extracting device and browser information
-- **Data Visualization**: Creating charts and graphs with matplotlib
-- **REST APIs**: Designing and implementing RESTful web services
-- **URL Validation**: Ensuring proper URL format and security
-
-Perfect for understanding web analytics, database design, and building production-ready web applications with comprehensive tracking capabilities.
+## Next Steps
+- Add **accounts** and per-user link management.
+- Switch unique counting to **cookies** and add **geo** data.
+- Enable **password-protected** and **editable** links.
+- Move charts **client-side** and add **rate limiting**.
+
+## Conclusion
+You built a URL shortener that doesn't just redirect — it *measures*, turning every click into an event you aggregate into a real dashboard with devices, browsers, referrers, and trends. The store-raw-events-then-`GROUP BY` pattern is exactly how production analytics works, and you wrapped it in a web UI, a REST API, and a CLI off a single engine. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/urlshorteneranalytics.py). Explore more web and analytics projects on Python Central Hub.
diff --git a/src/content/docs/projects/intermediate/url_expander.mdx b/src/content/docs/projects/intermediate/url_expander.mdx
deleted file mode 100644
index 67227a3a..00000000
--- a/src/content/docs/projects/intermediate/url_expander.mdx
+++ /dev/null
@@ -1,96 +0,0 @@
----
-title: URL Expander
-sidebar:
- order: 19
-hero:
- actions:
- - text: View on GitHub
- link: https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/url_expander.py
- icon: github
- variant: primary
----
-import FileCode from '../../../../components/FileCode.astro'
-
-## Abstract
-URL Expander is a Python project that takes shortened URLs and expands them to their original form. It demonstrates API usage, HTTP requests, and GUI development. This project is ideal for learning about web APIs, networking, and user interaction.
-
-## Prerequisites
-- Python 3.6 or above
-- requests (`pip install requests`)
-- tkinter (usually pre-installed)
-
-## Before you Start
-Install Python and requests. Prepare a list of shortened URLs for testing.
-
-## Getting Started
-1. Create a folder named `url-expander`.
-2. Create a file named `url_expander.py`.
-3. Copy the code below into your file.
-
-4. Run the script: `python url_expander.py`
-
-## Explanation
-
-### Code Breakdown
-1. **Import required modules.**
-```python
-import requests
-import tkinter as tk
-```
-2. **Expand shortened URL.**
-```python
-response = requests.head(short_url, allow_redirects=True)
-expanded_url = response.url
-```
-3. **Display result in GUI.**
-```python
-root = tk.Tk()
-root.title('URL Expander')
-# ...setup widgets for input and display...
-root.mainloop()
-```
-
-## Features
-- Expands shortened URLs
-- GUI for user interaction
-- Easy to extend for more features
-
-## How It Works
-- User enters shortened URL
-- App expands and displays original URL
-
-## GUI Components
-- **Entry box:** For URL input
-- **Button:** Expands URL
-- **Label:** Shows expanded URL
-
-## Use Cases
-- Reveal original links
-- Learn API and HTTP requests
-- Build web utilities
-
-## Next Steps
-You can enhance this project by:
-- Supporting batch expansion
-- Logging expanded URLs
-- Improving GUI design
-- Adding URL validation
-
-## Enhanced Version Ideas
-```python
-def expand_batch():
- # Expand multiple URLs at once
- pass
-
-def log_expansions():
- # Save expanded URLs to file
- pass
-```
-
-## Troubleshooting Tips
-- **Invalid URL:** Check input format
-- **No expansion:** Check internet connection
-- **GUI not showing:** Ensure Tkinter is installed
-
-## Conclusion
-This project teaches API usage, HTTP requests, and GUI basics. Extend it for more features and better user experience.
diff --git a/src/content/docs/projects/intermediate/weather-app-with-voice-commands.mdx b/src/content/docs/projects/intermediate/weather-app-with-voice-commands.mdx
index 9a0dc1db..54593f5b 100644
--- a/src/content/docs/projects/intermediate/weather-app-with-voice-commands.mdx
+++ b/src/content/docs/projects/intermediate/weather-app-with-voice-commands.mdx
@@ -1,5 +1,6 @@
---
title: Weather App with Voice Commands
+description: Build a voice-controlled weather app in Python — speak a city name, recognize it with SpeechRecognition, fetch conditions from OpenWeatherMap — then add text-to-speech replies, threading, wake-word listening, and robust error handling.
sidebar:
order: 99
hero:
@@ -12,66 +13,150 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+Talking to your computer feels like magic — and it's surprisingly approachable. This project chains two services together: **speech recognition** (turn your voice into text) and a **weather API** (turn a city name into a forecast). You click a button, say a city, and the app speaks-or-shows the weather. You'll learn how the microphone, recognizer, and Google's speech API cooperate, then upgrade the experience with spoken replies (text-to-speech), a responsive threaded UI, and the error handling that voice apps absolutely require.
-Build a Weather App that fetches weather information based on voice commands. The app uses speech recognition to capture user queries and displays weather data from an API. This project demonstrates API integration, speech recognition, and GUI development in Python.
+You will leave understanding:
+- The capture → recognize → act pipeline behind every voice assistant.
+- How `speech_recognition` wraps the mic and a cloud recognizer.
+- Why each failure mode (`UnknownValueError`, `RequestError`) needs its own handling.
+- How to close the loop with text-to-speech for a hands-free experience.
## Prerequisites
-- Python 3.6 or above
-- Text Editor or IDE
-- Basic understanding of Python syntax
-- Familiarity with Tkinter for GUI development
-- Knowledge of speech recognition and API requests
+- Python 3.6 or above.
+- A text editor or IDE, and a working **microphone**.
+- `pip install SpeechRecognition requests pyaudio` (PyAudio powers the mic; on Windows `pip install pipwin && pipwin install pyaudio` if it fails).
+- A free [OpenWeatherMap](https://openweathermap.org/api) API key.
+- Familiarity with the [Simple Weather Forecast App](./simple-weather-forecast-app) helps — it covers the API half.
## Getting Started
-### Creating a new project
-1. Create a new project folder and name it `weather_app_with_voice_commands`.
-2. Create a new file inside the folder and name it `weather_app_with_voice_commands.py`.
-3. Open the project folder in your favorite text editor or IDE.
-4. Copy the code below and paste it into the `weather_app_with_voice_commands.py` file.
+
+### Create the project
+1. Create a folder named `voice-weather`.
+2. Inside it, create `weather_app_with_voice_commands.py`.
+3. Install dependencies and set your API key in the code.
### Write the code
-## Key Features
-- Voice recognition to capture city names
-- Fetch weather data from an API
-- Display weather information in a user-friendly format
-- GUI interface for user interaction
+### Run it
+```cmd title="command"
+C:\Users\Your Name\voice-weather> python weather_app_with_voice_commands.py
+# Click "Speak", say a city like "London", and the weather appears.
+```
-## Explanation
+## Step-by-Step Explanation
-### Speech Recognition
-The app uses the `speech_recognition` library to capture voice input:
-```python title="weather_app_with_voice_commands.py" showLineNumbers={1}
+### 1. The recognizer and microphone
+```python title="weather_app_with_voice_commands.py"
recognizer = sr.Recognizer()
with sr.Microphone() as source:
audio = recognizer.listen(source)
city = recognizer.recognize_google(audio)
```
+`Recognizer` is the engine; `Microphone()` opens the default mic as a context manager (so it's released cleanly). `listen` records until you stop talking, and `recognize_google` sends the audio to Google's free speech API and returns text. That's the whole speech half in four lines.
+
+### 2. Handling what can go wrong
+```python title="weather_app_with_voice_commands.py"
+except sr.UnknownValueError:
+ messagebox.showerror("Error", "Sorry, I could not understand the audio.")
+except sr.RequestError:
+ messagebox.showerror("Error", "Could not request results, please check your internet connection.")
+```
+Voice input is *unreliable* — background noise, mumbling, or no internet all fail differently. `UnknownValueError` = "I heard you but couldn't parse it"; `RequestError` = "I couldn't reach the API." Distinct messages help the user fix the actual problem.
-### Fetching Weather Data
-Weather data is fetched from the OpenWeatherMap API:
-```python title="weather_app_with_voice_commands.py" showLineNumbers={1}
+### 3. Fetching the weather
+```python title="weather_app_with_voice_commands.py"
params = {"q": city, "appid": API_KEY, "units": "metric"}
response = requests.get(BASE_URL, params=params)
+data = response.json()
+if response.status_code == 200:
+ weather = data["weather"][0]["description"].capitalize()
+ temp = data["main"]["temp"]
+ ...
```
+Same pattern as a normal weather app — the recognized city flows straight into the API query. Always check `status_code` before parsing.
+
+### 4. Showing the result
+The formatted multi-line string lands in `result_label`. Next we'll *speak* it instead.
+
+## Close the Loop: Text-to-Speech
-### Displaying Results
-Weather information is shown in a Tkinter Label widget:
-```python title="weather_app_with_voice_commands.py" showLineNumbers={1}
-self.result_label.config(text=f"Weather: {weather}\nTemperature: {temp}°C")
+A voice app that only listens is half-built. Add spoken replies with `pyttsx3` (offline, no API):
+```python title="speak.py"
+import pyttsx3 # pip install pyttsx3
+engine = pyttsx3.init()
+
+def say(text):
+ engine.say(text)
+ engine.runAndWait()
+
+# after fetching:
+say(f"The weather in {city} is {weather}, {temp} degrees.")
```
+Now it's hands-free: ask, and it answers aloud.
+
+## Don't Freeze While Listening
+
+`recognizer.listen` blocks — on the main thread the whole window freezes during recording. Run it on a background thread and update the UI via `after`:
+```python title="threaded.py"
+import threading
+
+def get_weather_by_voice(self):
+ threading.Thread(target=self._listen_and_fetch, daemon=True).start()
-## Running the Application
-1. Save the file.
-2. Install required dependencies:
-```bash
-pip install speechrecognition requests
+def _listen_and_fetch(self):
+ # ... listen + recognize + fetch ...
+ self.root.after(0, lambda: self.result_label.config(text=result))
```
-3. Run the application:
-```bash
-python weather_app_with_voice_commands.py
+
+## Calibrate for Noise
+
+In a noisy room, recognition accuracy plummets. Let the recognizer sample the ambient level first:
+```python title="calibrate.py"
+with sr.Microphone() as source:
+ recognizer.adjust_for_ambient_noise(source, duration=1)
+ audio = recognizer.listen(source, timeout=5, phrase_time_limit=4)
```
+`adjust_for_ambient_noise` sets the energy threshold; `timeout`/`phrase_time_limit` stop it listening forever.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| `PyAudio` won't install | Missing build tools / wheels | `pipwin install pyaudio` (Windows); `brew install portaudio` (Mac) |
+| Always "couldn't understand" | Noisy mic / bad threshold | `adjust_for_ambient_noise` first |
+| Window freezes while listening | `listen` on the main thread | Listen on a background thread |
+| `RequestError` constantly | No internet (Google API needs it) | Check connection; use an offline engine like Vosk |
+| `KeyError` on weather data | Parsed before checking status | Guard with `status_code == 200` |
+| City misheard ("you York") | Speech ambiguity | Show the recognized text; let the user confirm/retry |
+
+## Variations to Try
+
+1. **Spoken replies** — add `pyttsx3` text-to-speech (above).
+2. **Wake word** — listen continuously for "weather" before acting.
+3. **Richer queries** — parse "weather in Paris tomorrow" for a forecast.
+4. **Offline recognition** — swap Google for Vosk (no internet needed).
+5. **Multi-language** — pass `language="es-ES"` to `recognize_google`.
+6. **Voice everything** — extend into a mini assistant (time, news, reminders).
+7. **Visual feedback** — animate a "listening" indicator while recording.
+
+## Real-World Applications
+- **Voice assistants** — the Alexa/Siri/Google pattern in miniature.
+- **Accessibility** — hands-free apps for users who can't type.
+- **Smart home** — voice control for devices and dashboards.
+- **In-car / kitchen apps** — eyes-and-hands-busy contexts.
+
+## Educational Value
+- **Speech recognition** — mics, recognizers, and cloud STT.
+- **Service chaining** — wiring two APIs into one flow.
+- **Failure handling** — voice and network are both unreliable.
+- **Accessible design** — multimodal input/output.
+
+## Next Steps
+- Add **text-to-speech** replies.
+- Move listening to a **background thread** and **calibrate** for noise.
+- Add a **wake word** and **richer query parsing**.
+- Try **offline recognition** with Vosk.
## Conclusion
-This Weather App with Voice Commands project is a great way to learn about API integration and speech recognition in Python. You can extend it by adding support for more languages, weather forecasts, or voice feedback.
+You built a voice-controlled weather app by chaining speech recognition to a weather API, then made it conversational with text-to-speech and responsive with threading. The capture → recognize → act → respond loop is exactly how every voice assistant works — you've just built a focused one. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/weather_app_with_voice_commands.py). Explore more voice and API projects on Python Central Hub.
diff --git a/src/content/docs/projects/intermediate/weather_app_with_voice_commands.mdx b/src/content/docs/projects/intermediate/weather_app_with_voice_commands.mdx
deleted file mode 100644
index 4f3bdb16..00000000
--- a/src/content/docs/projects/intermediate/weather_app_with_voice_commands.mdx
+++ /dev/null
@@ -1,105 +0,0 @@
----
-title: Weather App with Voice Commands
-sidebar:
- order: 12
-hero:
- actions:
- - text: View on GitHub
- link: https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/weather_app_with_voice_commands.py
- icon: github
- variant: primary
----
-import FileCode from '../../../../components/FileCode.astro'
-
-## Abstract
-Weather App with Voice Commands is a Python project that fetches weather data and allows users to interact using voice commands. It demonstrates API integration, speech recognition, and GUI development. This project is ideal for learning about voice interfaces, REST APIs, and real-world automation.
-
-## Prerequisites
-- Python 3.6 or above
-- requests (`pip install requests`)
-- speech_recognition (`pip install SpeechRecognition`)
-- tkinter (usually pre-installed)
-
-## Before you Start
-Install Python, requests, and SpeechRecognition. Obtain a free API key from a weather service (e.g., OpenWeatherMap).
-
-## Getting Started
-1. Create a folder named `weather-voice-app`.
-2. Create a file named `weather_app_with_voice_commands.py`.
-3. Copy the code below into your file.
-
-4. Run the script: `python weather_app_with_voice_commands.py`
-
-## Explanation
-
-### Code Breakdown
-1. **Import modules**
-```python
-import requests
-import speech_recognition as sr
-import tkinter as tk
-```
-2. **Fetch weather and process voice**
-```python
-def get_weather(city):
- # Fetch weather data from API
- pass
-
-def listen_for_command():
- # Use microphone to get user input
- pass
-```
-3. **GUI for weather display**
-```python
-root = tk.Tk()
-root.title('Weather App with Voice Commands')
-# ...setup widgets for weather info and voice input...
-root.mainloop()
-```
-
-## Features
-- Fetches weather data from API
-- Accepts voice commands
-- GUI for display
-- Easy to extend for more features
-
-## How It Works
-- Listens for voice input
-- Fetches weather for requested city
-- Displays info in GUI
-
-## GUI Components
-- **Label:** Shows weather info
-- **Button:** Starts voice recognition
-- **Entry box:** For manual city input
-
-## Use Cases
-- Hands-free weather updates
-- Learn API and voice integration
-- Build smart home apps
-
-## Next Steps
-You can enhance this project by:
-- Adding more voice commands
-- Supporting multiple languages
-- Improving GUI design
-- Integrating with other smart devices
-
-## Enhanced Version Ideas
-```python
-def add_language_support():
- # Recognize commands in other languages
- pass
-
-def integrate_with_smart_home():
- # Control devices based on weather
- pass
-```
-
-## Troubleshooting Tips
-- **API key errors:** Check your key
-- **Voice not recognized:** Check microphone
-- **GUI not showing:** Ensure Tkinter is installed
-
-## Conclusion
-This project teaches API usage, speech recognition, and GUI basics. Extend it for more features and smart automation.
diff --git a/src/content/docs/projects/intermediate/web-scraping-pipeline.mdx b/src/content/docs/projects/intermediate/web-scraping-pipeline.mdx
index 902e9e61..2e1ae018 100644
--- a/src/content/docs/projects/intermediate/web-scraping-pipeline.mdx
+++ b/src/content/docs/projects/intermediate/web-scraping-pipeline.mdx
@@ -1,6 +1,6 @@
---
title: Web Scraping Pipeline
-description: Professional web scraping system with automated scheduling, data processing, monitoring, and comprehensive analytics for large-scale data extraction.
+description: Build a production-grade web scraping pipeline in Python — per-domain rate limiting, robots.txt compliance, requests + Selenium, config-driven extraction rules, content-hash dedup, validation, scheduling, monitoring alerts, and a Flask dashboard.
sidebar:
order: 65
hero:
@@ -13,367 +13,157 @@ hero:
import FileCode from '../../../../components/FileCode.astro'
## Abstract
+Anyone can write a 10-line scraper. This project teaches the other 99% — the engineering that makes scraping **reliable, respectful, and maintainable** at scale. It's a full pipeline: a per-domain **rate limiter**, **robots.txt** compliance, a dual `requests`/**Selenium** engine (for JavaScript-heavy sites and infinite scroll), **config-driven extraction rules** so you scrape new sites without new code, **content-hash deduplication**, data validation, scheduling, monitoring with alerts, and a Flask dashboard. If [Basic Web Scraper](../../beginners/basicwebscrapper) was your first scraper, this is what a real one looks like.
-Build a comprehensive web scraping pipeline that automates data extraction from multiple websites with advanced features like scheduling, monitoring, rate limiting, data processing, and comprehensive analytics.
+You will leave understanding:
+- The ethics and mechanics of polite scraping: robots.txt, rate limits, delays, headers.
+- When to use `requests` vs. **Selenium**, and how to handle infinite scroll.
+- Why extraction rules should be **data, not code**.
+- How content hashing deduplicates and how monitoring keeps a pipeline healthy.
## Prerequisites
-
-- Python 3.8 or above
-- Text Editor or IDE
-- Solid understanding of Python syntax and OOP concepts
-- Knowledge of web technologies (HTML, CSS, JavaScript)
-- Familiarity with HTTP protocols and web requests
-- Understanding of data processing and analysis
-- Experience with scheduling and automation
-- Basic knowledge of web scraping ethics and robots.txt
+- Python 3.7 or above.
+- A text editor or IDE.
+- `pip install requests beautifulsoup4 pandas selenium flask plotly` (+ ChromeDriver for Selenium).
+- Comfort with HTML/CSS selectors and the basics from [Advanced Web Scraping](./advanced-web-scraping).
+- Understanding of HTTP and SQL.
## Getting Started
-### Create a new project
-1. Create a new project folder and name it `webScrapingPipeline`.
-2. Create a new file and name it `webscrapingpipeline.py`.
-3. Install required dependencies: `pip install requests beautifulsoup4 pandas sqlite3 selenium flask plotly nltk textblob wordcloud matplotlib seaborn schedule`
-4. Open the project folder in your favorite text editor or IDE.
-5. Copy the code below and paste it into your `webscrapingpipeline.py` file.
+### Create the project
+1. Create a folder named `scraping-pipeline`.
+2. Inside it, create `webscrapingpipeline.py`.
+3. Install dependencies. Selenium features need Chrome + ChromeDriver (optional — it degrades gracefully).
### Write the code
-1. Add the following code to your `webscrapingpipeline.py` file.
-
-2. Save the file.
-3. Run the following command to run the application.
-```cmd title="command" showLineNumbers{1}
-C:\Users\username\Documents\webScrapingPipeline> python webscrapingpipeline.py
-🕷️ Web Scraping Pipeline
-==================================================
-🚀 Starting scraping platform...
-🌐 Dashboard available at: http://localhost:5000
-🔍 Scraping engine ready
-📊 Analytics system loaded
-```
-
-## 🎯 What You'll Build
-
-A professional-grade web scraping system featuring:
+
-### Core Scraping Features
-- **Multi-Method Extraction**: Support for requests and Selenium-based scraping
-- **Intelligent Rate Limiting**: Respectful scraping with domain-specific rate limits
-- **Robots.txt Compliance**: Automatic robots.txt checking and compliance
-- **Duplicate Detection**: Hash-based duplicate data detection
-- **Error Handling**: Comprehensive error handling and retry mechanisms
-
-### Automation & Scheduling
-- **Flexible Scheduling**: Support for interval, daily, and weekly schedules
-- **Background Processing**: Non-blocking scheduled execution
-- **Project Management**: Multiple independent scraping projects
-- **Task Monitoring**: Real-time monitoring of scraping tasks
-
-### Data Processing & Validation
-- **Data Cleaning**: Text normalization and cleaning
-- **Sentiment Analysis**: Automatic sentiment analysis of text content
-- **Keyword Extraction**: Intelligent keyword extraction from content
-- **Data Validation**: Configurable validation rules and data quality checks
-- **Type Conversion**: Automatic data type conversion and formatting
+### Run it
+```cmd title="command"
+C:\Users\Your Name\scraping-pipeline> python webscrapingpipeline.py
+# Define a project (target URLs + extraction rules), run it, watch the dashboard.
+```
-### Analytics & Reporting
-- **Performance Metrics**: Success rates, execution times, error analysis
-- **Trend Analysis**: Daily and weekly scraping trends
-- **Data Quality Metrics**: Uniqueness, completeness, and size analysis
-- **Export Capabilities**: CSV, JSON, and Excel export options
+## Scrape Responsibly (Read This First)
-### Web Interface
-- **Dashboard**: Comprehensive overview of all scraping projects
-- **Project Management**: Create, edit, and monitor scraping projects
-- **Real-time Monitoring**: Live status updates and performance metrics
-- **Interactive Analytics**: Charts and graphs for data visualization
+The pipeline bakes in good citizenship — copy this mindset into every scraper you write:
+- **Honor robots.txt.** Check what a site permits before fetching.
+- **Rate-limit per domain.** Don't hammer one server.
+- **Add delays** and a real **User-Agent**. Behave like a browser, not a flood.
+- **Cache and dedupe.** Don't re-fetch unchanged pages.
-## 🏗️ Architecture Overview
+Scraping aggressively can get you IP-banned, break sites, or cross legal lines. The engineering below is also the *ethics*.
-The system is built with several interconnected components:
+## Step-by-Step Explanation
-```python title="webscrapingpipeline.py" showLineNumbers{1}
+### 1. Per-domain rate limiting
+```python title="webscrapingpipeline.py"
class RateLimiter:
- """Implements domain-specific rate limiting for respectful scraping."""
-
- def __init__(self, max_requests=10, time_window=60):
- self.max_requests = max_requests
- self.time_window = time_window
- self.requests = defaultdict(deque)
-
def is_allowed(self, domain):
- """Check if request to domain is allowed based on rate limit."""
- # Rate limiting logic with time window tracking
-
-class WebScraper:
- """Main scraping engine with support for multiple extraction methods."""
-
- def scrape_url(self, url, scraping_config):
- """Scrape a single URL with given configuration."""
- # Robots.txt checking, rate limiting, and data extraction
-
- def _extract_data(self, html_content, extraction_rules):
- """Extract data using CSS selectors, regex, and XPath."""
- # BeautifulSoup-based data extraction with multiple rule types
-
-class DataProcessor:
- """Advanced data processing and validation engine."""
-
- def process_scraped_data(self, raw_data, processing_rules):
- """Process raw scraped data using configurable rules."""
- # Text cleaning, sentiment analysis, and data transformation
-
-class ScrapingScheduler:
- """Automated scheduling system for scraping projects."""
-
- def _run_scraping_project(self, project_id):
- """Execute a scheduled scraping project."""
- # Project execution with logging and data storage
+ now = time.time()
+ dq = self.requests[domain]
+ while dq and dq[0] <= now - self.time_window: # drop old timestamps
+ dq.popleft()
+ if len(dq) < self.max_requests:
+ dq.append(now)
+ return True
+ return False
```
-
-## 💾 Database Schema
-
-The system uses SQLite with a comprehensive schema:
-
-```python title="webscrapingpipeline.py" showLineNumbers{35}
-def init_database(self):
- """Create database tables for web scraping pipeline."""
-
- # Scraping projects - project configurations
- # Scraped data - extracted and processed data
- # Scraping logs - execution history and monitoring
- # Validation rules - data quality enforcement
- # Monitoring alerts - automated notifications
- # Exported reports - export history tracking
- # Site metadata - robots.txt and crawl policies
+A `deque` of timestamps **per domain** implements a sliding-window limit ("≤10 requests / 60s"). Old timestamps fall off the left; if the window is full, the request is denied. This is the same algorithm APIs use to rate-limit *you*.
+
+### 2. robots.txt compliance
+```python title="webscrapingpipeline.py"
+rp = RobotFileParser()
+rp.set_url(urljoin(domain, '/robots.txt'))
+rp.read()
+return rp.can_fetch('*', url) # cached per domain
```
-
-## 🔧 Core Features
-
-### 1. Intelligent Web Scraping
-
-```python title="webscrapingpipeline.py" showLineNumbers{200}
-def scrape_url(self, url, scraping_config):
- """Comprehensive URL scraping with multiple safeguards."""
-
- # Check robots.txt compliance
- if not self.check_robots_txt(url):
- return {'status': 'skipped', 'error': 'Robots.txt disallows crawling'}
-
- # Apply rate limiting
- domain = urlparse(url).netloc
- if not self.rate_limiter.is_allowed(domain):
- wait_time = self.rate_limiter.wait_time(domain)
- return {'status': 'rate_limited', 'error': f'Rate limited. Wait {wait_time:.1f} seconds'}
-
- # Choose appropriate scraping method
- if scraping_config.get('use_selenium', False):
- response_data = self._scrape_with_selenium(url, scraping_config)
- else:
- response_data = self._scrape_with_requests(url, scraping_config)
+Python's standard `RobotFileParser` reads a site's rules; `can_fetch` tells you if a path is allowed. The result is cached per domain so you fetch `/robots.txt` once. Skipping this check is how scrapers get banned.
+
+### 3. Two engines: requests vs. Selenium
+```python title="webscrapingpipeline.py"
+if scraping_config.get('use_selenium', False) and self.use_selenium:
+ response_data = self._scrape_with_selenium(url, scraping_config)
+else:
+ response_data = self._scrape_with_requests(url, scraping_config)
```
-
-### 2. Advanced Data Processing
-
-```python title="webscrapingpipeline.py" showLineNumbers{400}
-def process_scraped_data(self, raw_data, processing_rules):
- """Multi-stage data processing pipeline."""
-
- processed_data = {}
-
- for field_name, value in raw_data.items():
- if field_name in processing_rules:
- # Apply text cleaning, sentiment analysis, validation
- processed_data[field_name] = self._apply_processing_rules(
- value, processing_rules[field_name]
- )
- else:
- processed_data[field_name] = value
-
- # Calculate derived fields (concatenation, calculations)
- if 'derived_fields' in processing_rules:
- for derived_field, rule in processing_rules['derived_fields'].items():
- processed_data[derived_field] = self._calculate_derived_field(processed_data, rule)
+**`requests`** is fast and cheap — use it for static HTML. **Selenium** drives a real headless browser — necessary when content is rendered by JavaScript. The pipeline picks per-config and **degrades gracefully** to `requests` if ChromeDriver isn't installed. Choosing the cheapest tool that works is a core scraping skill.
+
+### 4. Handling infinite scroll
+```python title="webscrapingpipeline.py"
+last_height = self.driver.execute_script("return document.body.scrollHeight")
+while True:
+ self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
+ time.sleep(2)
+ new_height = self.driver.execute_script("return document.body.scrollHeight")
+ if new_height == last_height: # nothing new loaded
+ break
+ last_height = new_height
```
-
-### 3. Automated Scheduling
-
-```python title="webscrapingpipeline.py" showLineNumbers{550}
-def _schedule_project(self, project_id, project_name, schedule_config):
- """Configure automated project scheduling."""
-
- if schedule_config['type'] == 'interval':
- if schedule_config['unit'] == 'minutes':
- schedule.every(schedule_config['value']).minutes.do(
- self._run_scraping_project, project_id
- )
- elif schedule_config['unit'] == 'hours':
- schedule.every(schedule_config['value']).hours.do(
- self._run_scraping_project, project_id
- )
- elif schedule_config['type'] == 'daily':
- schedule.every().day.at(schedule_config['time']).do(
- self._run_scraping_project, project_id
- )
+Scroll to the bottom, wait for new content, repeat until the page stops growing — the standard trick for lazy-loaded feeds.
+
+### 5. Extraction rules as data, not code
+The scraper reads *what to extract* from a config (CSS selectors per field), so adding a new site is a config edit, not a code change:
+```python title="config-example.py"
+extraction_rules = {
+ "title": {"selector": "h1.product-title", "type": "text"},
+ "price": {"selector": "span.price", "type": "text"},
+ "image": {"selector": "img.main", "type": "attr", "attr": "src"},
+}
```
+Separating rules from engine is what lets one scraper serve hundreds of sites — the difference between a script and a *platform*.
-### 4. Comprehensive Analytics
-
-```python title="webscrapingpipeline.py" showLineNumbers{650}
-def generate_project_report(self, project_id, days_back=30):
- """Generate detailed project performance report."""
-
- # Get scraping statistics
- stats = pd.read_sql_query('''
- SELECT
- COUNT(*) as total_attempts,
- SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful_scrapes,
- AVG(execution_time) as avg_execution_time,
- MAX(execution_time) as max_execution_time
- FROM scraping_logs
- WHERE project_id = ? AND timestamp >= ?
- ''', conn, params=[project_id, start_date])
-
- # Analyze daily trends, error patterns, and data quality
- return comprehensive_report
+### 6. Deduplication by content hash
+```python title="webscrapingpipeline.py"
+import hashlib
+data_hash = hashlib.sha256(raw_data.encode()).hexdigest()
+# status CHECK(... 'duplicate'): skip storing if this hash already exists
```
-
-## 🚀 Getting Started
-
-### 1. Install Dependencies
-
-```bash title="terminal" showLineNumbers{1}
-pip install requests beautifulsoup4 pandas sqlite3 selenium flask plotly
-pip install nltk textblob wordcloud matplotlib seaborn schedule
-```
-
-### 2. Run the Application
-
-```python title="webscrapingpipeline.py" showLineNumbers{850}
-def main():
- """Main function with interface selection."""
-
- choice = input("\nChoose interface:\n1. Web Interface\n2. CLI Demo\nEnter choice (1-2): ")
-
- if choice == '2':
- # CLI demo with sample scraping
- print("🕷️ Running CLI Demo...")
- # Execute demo scraping with test URLs
- else:
- # Launch Flask web interface
- app = ScrapingWebInterface()
- app.run()
-```
-
-### 3. Web Interface Features
-
-The Flask-based web interface provides:
-
-- **Dashboard**: Overview of all scraping projects and system status
-- **Project Management**: Create, edit, and configure scraping projects
-- **Live Monitoring**: Real-time status updates and performance metrics
-- **Data Export**: Download scraped data in multiple formats
-- **Analytics**: Interactive charts and comprehensive reporting
-
-## 📊 Sample Output
-
-### Project Creation
-```
-🕷️ Web Scraping Pipeline
-==================================================
-🚀 Starting scraping platform...
-🌐 Access the dashboard at: http://localhost:5000
-
-🔥 Scraping Features:
- - Multi-site data extraction
- - Automated scheduling and monitoring
- - Rate limiting and robots.txt compliance
- - Data validation and processing
- - Export in multiple formats
- - Comprehensive analytics and reporting
- - Web interface for easy management
-```
-
-### Scraping Execution
-```
-Starting scheduled scraping: News Website Monitor
-Scraping: https://example-news.com/latest
- ✅ Success: 2,847 characters extracted
- 📊 Data: {'title': 'Breaking News Today', 'articles': [...]}
-
-Scraping: https://example-news.com/technology
- ✅ Success: 1,923 characters extracted
- 📊 Data: {'title': 'Tech Updates', 'articles': [...]}
-
-Completed scheduled scraping: News Website Monitor
-Success Rate: 95.2% | Avg Time: 1.3s | Total Records: 156
-```
-
-### Analytics Report
-```
-📈 Project Performance Report
-================================
-Project: E-commerce Product Tracker
-Period: Last 30 days
-
-📊 Summary Statistics:
- • Total Attempts: 4,320
- • Successful Scrapes: 4,118 (95.3%)
- • Failed Scrapes: 202 (4.7%)
- • Average Execution Time: 2.1 seconds
- • Data Records Collected: 3,847
-
-🔍 Data Quality:
- • Unique Records: 3,692 (95.9%)
- • Average Data Size: 1.2 KB
- • Duplicate Rate: 4.1%
-
-⚠️ Top Errors:
- • Connection timeout: 89 occurrences (44.1%)
- • Rate limit exceeded: 67 occurrences (33.2%)
- • Invalid selector: 46 occurrences (22.8%)
-```
-
-## 🎓 Learning Objectives
-
-### Web Scraping Fundamentals
-- **HTTP Requests**: Understanding web protocols and response handling
-- **HTML Parsing**: DOM navigation and data extraction techniques
-- **CSS Selectors**: Advanced selector patterns for precise targeting
-- **JavaScript Rendering**: When and how to use browser automation
-
-### System Design Principles
-- **Rate Limiting**: Implementing respectful scraping practices
-- **Error Handling**: Comprehensive error management and recovery
-- **Data Pipelines**: Building robust data processing workflows
-- **Monitoring**: System health tracking and alerting
-
-## Explanation
-
-1. The `WebScrapingPipeline` class orchestrates the complete scraping system with Flask web interface.
-2. The `RateLimiter` ensures respectful scraping practices with domain-specific rate limiting.
-3. The `WebScraper` handles both requests-based and Selenium browser automation for data extraction.
-4. The `DataProcessor` provides text analysis, sentiment analysis, and keyword extraction.
-5. The `ScrapingScheduler` manages automated scheduling with flexible timing options.
-6. The `ProjectManager` organizes multiple scraping projects with individual configurations.
-7. Analytics engine provides comprehensive performance metrics and trend analysis.
-8. Database design supports project management, data storage, and historical tracking.
-9. Error handling includes retry mechanisms and comprehensive logging.
-10. Web dashboard provides real-time monitoring and project management.
-11. Robots.txt compliance ensures ethical scraping practices.
-12. Export capabilities support multiple data formats for further analysis.
+Hash the scraped content; if you've seen that hash, it's a duplicate — skip it. Cheap, exact change-detection that avoids storing the same page twice (the same idea as the [File Version Control System](./basic-file-version-control-system)).
+
+### 7. Logging, validation, and monitoring
+Every fetch writes a row to `scraping_logs` (status, response code, timing). Validation rules check scraped fields (required/type/format/range). Monitoring alerts fire when error rates or data-quality thresholds are breached. **Observability is what separates a pipeline from a one-off script** — when a site changes its HTML, you want an alert, not silent garbage.
+
+## Common Mistakes
+
+| Problem | Cause | Fix |
+|---|---|---|
+| IP banned mid-scrape | Too fast, no delays | Rate-limit per domain, add random delays |
+| Empty results | JS-rendered content | Use Selenium for dynamic pages |
+| `WebDriverException` | ChromeDriver missing/mismatched | Install matching driver; code falls back to `requests` |
+| Same data stored repeatedly | No dedup | Hash content; skip seen hashes |
+| Scraper silently breaks | Site changed its HTML | Validation rules + monitoring alerts |
+| Legal/ToS trouble | Ignored robots.txt / terms | Check robots.txt; respect site terms |
+| Memory blows up on infinite scroll | Scrolling forever | Cap iterations; stop when height stops changing |
+
+## Variations to Try
+
+1. **Proxy rotation** — distribute requests across IPs (responsibly).
+2. **Async engine** — `aiohttp` + `asyncio` for high-throughput static scraping.
+3. **Distributed scraping** — a task queue (Celery/RQ) across workers.
+4. **Auto-detect extraction** — infer selectors instead of hard-coding.
+5. **Change alerts** — notify when a tracked value (e.g. price) changes.
+6. **Incremental crawls** — only re-fetch pages older than N hours.
+7. **Export targets** — push to CSV, Parquet, or a data warehouse.
+8. **CAPTCHA-aware backoff** — detect blocks and pause gracefully.
+
+## Real-World Applications
+- **Price monitoring** — track competitor or retail prices.
+- **Market research** — aggregate listings, reviews, jobs.
+- **News & content aggregation** — feed pipelines (see [News Aggregator](./news-aggregator)).
+- **Data engineering** — populate warehouses for analytics/ML.
+
+## Educational Value
+- **Ethical scraping** — robots.txt, rate limits, delays, identification.
+- **Tool selection** — requests vs. Selenium, graceful degradation.
+- **Pipeline design** — config-driven rules, dedup, validation, monitoring.
+- **Observability** — logging and alerting for long-running jobs.
## Next Steps
-
-Congratulations! You have successfully created a Web Scraping Pipeline in Python. Experiment with the code and see if you can modify the application. Here are a few suggestions:
-- Add machine learning for content classification
-- Implement distributed scraping with multiple workers
-- Create advanced data visualization dashboards
-- Add cloud storage integration for scalability
-- Implement real-time monitoring and alerting
-- Create API endpoints for programmatic access
-- Add support for JavaScript-heavy websites
-- Integrate with data analysis and BI tools
+- Add an **async engine** for static-page throughput.
+- Implement **change alerts** and **incremental crawls**.
+- Add **proxy rotation** and a **task queue** for scale.
+- Push results to a **data warehouse**.
## Conclusion
-
-In this project, you learned how to create a professional Web Scraping Pipeline in Python with automation, monitoring, and analytics. You explored ethical web scraping, data processing, scheduling systems, and building comprehensive data extraction platforms. You can find the source code on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/webscrapingpipeline.py)
+You built a scraping *pipeline*, not a script: it respects robots.txt, rate-limits per domain, picks requests or Selenium per site, extracts via swappable config rules, dedupes by content hash, validates output, and monitors itself. That engineering — and the ethics baked into it — is the difference between a scraper that survives in production and one that gets banned by lunch. Full source on [GitHub](https://github.com/Ravikisha/PythonCentralHub/blob/main/projects/intermediate/webscrapingpipeline.py). Explore more data-engineering projects on Python Central Hub.
diff --git a/src/content/docs/tutorials/Boolean.mdx b/src/content/docs/tutorials/Boolean.mdx
index 1625e69f..3054f52d 100644
--- a/src/content/docs/tutorials/Boolean.mdx
+++ b/src/content/docs/tutorials/Boolean.mdx
@@ -59,6 +59,23 @@ Ten is greater than nine!
Logical operators in Python are used to perform operations on Boolean values. The three primary logical operators are `and`, `or`, and `not`.
+**Diagram**:
+```mermaid title="logical operators" desc="Truth outcomes for and, or, not"
+graph TD
+ A[Two boolean inputs] --> B["and: True only if BOTH true"]
+ A --> C["or: True if AT LEAST ONE true"]
+ D[One boolean input] --> E["not: flips the value"]
+```
+
+The full truth table for `and` and `or`:
+
+| `a` | `b` | `a and b` | `a or b` |
+| :---: | :---: | :---: | :---: |
+| True | True | True | True |
+| True | False | False | True |
+| False | True | False | True |
+| False | False | False | False |
+
### `and` Operator
The `and` operator returns `True` if both operands are true; otherwise, it returns `False`.
@@ -111,6 +128,39 @@ C:\Users\Your Name> python booleans.py
False
```
+### Short-Circuit Evaluation
+
+Python stops evaluating a logical expression as soon as the result is known. With `and`, if the first operand is falsy the second is never checked; with `or`, if the first is truthy the second is skipped. This is called **short-circuiting**.
+
+```python title="short_circuit.py" showLineNumbers{1} {1-3}
+def expensive():
+ print("expensive() was called")
+ return True
+
+False and expensive() # expensive() is NEVER called
+True or expensive() # expensive() is NEVER called
+```
+
+A practical use is guarding against errors — the right side runs only when it is safe:
+
+```python title="guard.py" showLineNumbers{1} {2}
+name = ""
+if name and name[0] == "A": # name[0] only runs if name is non-empty
+ print("Starts with A")
+```
+
+### `and` / `or` Return Operands, Not Just Booleans
+
+A subtle but useful fact: `and` and `or` return one of the **actual operands**, not necessarily `True`/`False`. `or` returns the first truthy value; `and` returns the first falsy value (or the last operand).
+
+```python title="operands.py" showLineNumbers{1} {1-3}
+print("" or "default") # default -> first truthy value
+print("Alice" or "guest") # Alice -> first truthy value
+print(5 and 0 and 9) # 0 -> first falsy value
+```
+
+This powers the common "default value" idiom: `username = entered_name or "Anonymous"`.
+
## Comparison Operators
Comparison operators are used to compare values and return Boolean results. These operators include `==` (equal), `!=` (not equal), `<` (less than), `>` (greater than), `<=` (less than or equal to), and `>=` (greater than or equal to).
@@ -237,6 +287,29 @@ False
False
```
+### Truthy and Falsy Values
+
+Every Python object is either **truthy** or **falsy** when used in a boolean context (like an `if`). Only a small set of values are falsy; everything else is truthy.
+
+| Falsy values | Truthy values |
+| :--- | :--- |
+| `False`, `None` | Any non-zero number |
+| `0`, `0.0`, `0j` | Any non-empty string |
+| `""` (empty string) | Any non-empty list/tuple/dict/set |
+| `[]`, `()`, `{}`, `set()` | Most objects |
+
+This lets you write clean checks — `if my_list:` instead of `if len(my_list) > 0:`.
+
+### Booleans Are Integers
+
+As noted earlier, `bool` is a subclass of `int`: `True` equals `1` and `False` equals `0`. This means you can do arithmetic with them, which is handy for counting:
+
+```python title="bool_int.py" showLineNumbers{1} {1-3}
+votes = [True, False, True, True, False]
+print(sum(votes)) # 3 -> counts how many are True
+print(True + True) # 2
+```
+
## Conclusion
Boolean logic is an integral part of Python programming, enabling developers to create conditions, make decisions, and control the flow of their code. Understanding Boolean values, logical operators, and their applications in conditional statements, functions, and loops is essential for writing clear, efficient, and effective Python code.
diff --git a/src/content/docs/tutorials/Comment.mdx b/src/content/docs/tutorials/Comment.mdx
index 44cec1e3..af14f31a 100644
--- a/src/content/docs/tutorials/Comment.mdx
+++ b/src/content/docs/tutorials/Comment.mdx
@@ -9,6 +9,25 @@ import DataCampExercise from "../../../components/DataCampExercise.astro";
Comments are used in Python to add explanations or notes to the code. They are not executed and do not affect the program's functionality. Comments are crucial for enhancing code readability and for providing context to readers.
+A good comment explains **why** the code does something, not **what** it does — the code itself already shows the *what*.
+
+```python title="why_not_what.py" showLineNumbers{1} {1,4}
+# Bad: restates the obvious
+x = x + 1 # add 1 to x
+
+# Good: explains the reason
+x = x + 1 # advance to the next page; pages are 1-indexed
+```
+
+**Diagram**:
+```mermaid title="how Python treats comments" desc="The interpreter ignores everything after the hash"
+graph LR
+ A[Source line] --> B{Contains #?}
+ B -->|Yes| C[Run code before #]
+ C --> D[Ignore text after #]
+ B -->|No| E[Run the whole line]
+```
+
## Single-line Comments
In Python, single-line comments start with the `#` symbol. Anything following the `#` on that line is considered a comment.
@@ -66,6 +85,48 @@ result = add_numbers(5, 3)
print("Result:", result)
```
+### Accessing a Docstring
+
+Unlike ordinary comments, docstrings are kept by Python at runtime. You can read them with the built-in `help()` function or the `__doc__` attribute — this is how editors and `help()` show documentation.
+
+```python title="docstring_access.py" showLineNumbers{1} {1}
+print(add_numbers.__doc__) # prints the docstring text
+help(add_numbers) # prints formatted help
+```
+
+## Commenting Out Code
+
+During debugging it is common to temporarily disable a line by turning it into a comment. Most editors do this for a whole selection with `Ctrl + /`.
+
+```python title="disable.py" showLineNumbers{1} {2}
+print("This runs")
+# print("This line is disabled for now")
+print("This runs too")
+```
+
+## TODO and FIXME Conventions
+
+Teams use tagged comments so tools and editors can find pending work. Common tags include `TODO`, `FIXME`, and `NOTE`:
+
+```python title="tags.py" showLineNumbers{1}
+# TODO: handle negative numbers
+# FIXME: this crashes when the list is empty
+# NOTE: prices are stored in cents, not dollars
+```
+
+## The Shebang Line
+
+On Linux and macOS, the first line of an executable script can be a special comment called a **shebang**. It tells the system which interpreter to use, so the script can be run directly:
+
+```python title="script.py" showLineNumbers{1} {1}
+#!/usr/bin/env python3
+print("Run me directly with ./script.py")
+```
+
+:::note
+The shebang is only meaningful as the **first line** of the file. Anywhere else it is just an ordinary comment.
+:::
+
## Best Practices for Comments
1. **Be Clear and Concise:** Write comments that are easy to understand. Avoid unnecessary comments that restate the obvious.
diff --git a/src/content/docs/tutorials/DataType.mdx b/src/content/docs/tutorials/DataType.mdx
index 24d04b18..bee802f3 100644
--- a/src/content/docs/tutorials/DataType.mdx
+++ b/src/content/docs/tutorials/DataType.mdx
@@ -11,6 +11,45 @@ import DataCampExercise from "../../../components/DataCampExercise.astro";
Data types in Python are a fundamental concept that plays a crucial role in defining the nature of variables and how they behave during operations. Python is a dynamically-typed language, which means that the interpreter can determine the type of a variable at runtime. This flexibility allows for more concise and expressive code. Let's delve into the essential data types in Python.
+**Diagram**:
+```mermaid title="Python built-in data types" desc="The main categories of built-in types"
+graph TD
+ A[Python Data Types] --> B[Numeric]
+ A --> C[Sequence]
+ A --> D[Set]
+ A --> E[Mapping]
+ A --> F[Boolean]
+ A --> G[None]
+ B --> B1[int]
+ B --> B2[float]
+ B --> B3[complex]
+ C --> C1[str]
+ C --> C2[list]
+ C --> C3[tuple]
+ D --> D1[set]
+ D --> D2[frozenset]
+ E --> E1[dict]
+```
+
+### Dynamic Typing
+
+Because Python infers types at runtime, the *same* variable can hold different types over its life — the type belongs to the value, not the variable.
+
+```python title="dynamic.py" showLineNumbers{1} {1-4}
+x = 42 # x is now an int
+print(type(x)) #
+x = "hello" # x is now a str — perfectly legal
+print(type(x)) #
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-3}
+C:\Users\Your Name> python dynamic.py
+
+
+```
+
## Numeric Data Types
### Integers
@@ -218,6 +257,30 @@ There are many more data types in Python, but the ones listed above are the most
+## Mutable vs. Immutable Types
+
+A crucial distinction: some objects can be changed after creation (**mutable**), and some cannot (**immutable**). Trying to change an immutable object creates a new object instead.
+
+| Mutable (can change) | Immutable (cannot change) |
+| :--- | :--- |
+| `list`, `dict`, `set`, `bytearray` | `int`, `float`, `complex`, `bool` |
+| | `str`, `tuple`, `frozenset`, `bytes` |
+
+```python title="mutability.py" showLineNumbers{1} {1-7}
+# Lists are mutable - same object is modified
+numbers = [1, 2, 3]
+numbers[0] = 99
+print(numbers) # [99, 2, 3]
+
+# Strings are immutable - this raises an error
+text = "hello"
+# text[0] = "H" # TypeError: 'str' object does not support item assignment
+```
+
+:::note
+You can check whether two names point to the *same* object with the `id()` function or the `is` operator. Mutating a mutable object is visible through every name that references it.
+:::
+
## Conclusion
Understanding Python's data types is fundamental to writing effective and efficient code. Python's flexibility in handling various data types makes it suitable for a wide range of applications, from simple scripts to complex data analysis and machine learning tasks. As you continue your journey in Python programming, a solid grasp of data types will empower you to manipulate and process data effectively.
diff --git a/src/content/docs/tutorials/Datatype Casting.mdx b/src/content/docs/tutorials/Datatype Casting.mdx
index 94089b7a..c427ee5a 100644
--- a/src/content/docs/tutorials/Datatype Casting.mdx
+++ b/src/content/docs/tutorials/Datatype Casting.mdx
@@ -10,6 +10,17 @@ import DataCampExercise from "../../../components/DataCampExercise.astro";
## Navigating Data Types: A Guide to Data Type Casting in Python
Data type casting, also known as type conversion, is a fundamental concept in Python that allows you to change the data type of a variable or value. This process is crucial when working with different data types and ensures the compatibility of variables in various operations. In this comprehensive guide, we'll explore the basics of data type casting in Python, including the built-in functions used for conversion and best practices.
+There are two kinds of conversion. **Implicit** conversion is done automatically by Python; **explicit** conversion is done by you, using a built-in function.
+
+**Diagram**:
+```mermaid title="implicit vs explicit casting" desc="The two kinds of type conversion in Python"
+graph TD
+ A[Type Conversion] --> B[Implicit: Python does it]
+ A --> C[Explicit: you call a function]
+ B --> B1["int + float -> float automatically"]
+ C --> C1["int('42'), float(x), str(n)"]
+```
+
## Understanding Data Types in Python
Before we dive into the details of data type casting, let's take a moment to understand the different data types in Python. Python is a dynamically typed language, which means that the interpreter automatically assigns a data type to a variable based on the value it holds. For example, if you assign the value `5` to a variable, the interpreter will assign the integer data type to that variable. Similarly, if you assign the value `5.0` to a variable, the interpreter will assign the float data type to that variable.
@@ -72,8 +83,10 @@ Python also allows you to explicitly convert one data type to another using buil
| `bytes()` | Converts a value to bytes |
| `bytearray()` | Converts a value to a byte array |
| `complex()` | Converts a value to a complex number |
-| `range()` | Converts a value to a range |
-| `None` | Converts a value to None |
+
+:::caution
+`range()` and `None` are **not** casting functions. `range()` generates a sequence of numbers, and `None` is a value (not a converter). They are sometimes mistakenly listed as casts — do not treat them that way.
+:::
## Integer Conversion
@@ -98,6 +111,20 @@ C:\Users\Your Name> python casting.py
In the above example, we have used the `int()` function to convert a float to an integer. The `int()` function takes a single argument and returns an integer. The result of the `int()` function is then assigned to the variable `y`. The value of `y` is then printed to the console.
+:::caution
+**Two things to watch with `int()`:**
+
+1. **Data loss** — converting a float to an int *truncates* (chops off) the decimal part, it does not round: `int(5.9)` is `5`, not `6`.
+2. **Conversion errors** — `int()` only parses strings that look like whole numbers. `int("3.14")` and `int("hello")` both raise `ValueError`.
+
+```python title="casting_errors.py" showLineNumbers{1} {1-3}
+print(int(5.9)) # 5 (truncated, not rounded)
+# print(int("3.14")) # ValueError: invalid literal for int()
+print(int(float("3.14"))) # 3 -> go via float first
+```
+Wrap risky conversions in `try/except ValueError` when the value comes from user input or a file.
+:::
+
## Float Conversion
#### `float()` Function
The `float()` function converts a value to a float. The following example demonstrates how to use the `float()` function in Python:
diff --git a/src/content/docs/tutorials/GetStarted.mdx b/src/content/docs/tutorials/GetStarted.mdx
index 938d5c66..d7b297e1 100644
--- a/src/content/docs/tutorials/GetStarted.mdx
+++ b/src/content/docs/tutorials/GetStarted.mdx
@@ -9,7 +9,27 @@ sidebar:
To start coding in Python, you need to have Python installed on your computer. Follow the installation guide on our [Installation Page](/tutorials/installation) if you haven't done so already.
-Once Python is installed, you can run Python code using the command line or terminal.
+Once Python is installed, you can run Python code in several ways. Pick the one that fits what you are doing.
+
+**Diagram**:
+```mermaid title="ways to run Python" desc="Different ways to execute Python code"
+graph TD
+ A[Write Python code] --> B[Interactive shell / REPL]
+ A --> C[Run a .py file]
+ A --> D[One-liner with -c]
+ A --> E[IDE Run button]
+ B --> F[Output]
+ C --> F
+ D --> F
+ E --> F
+```
+
+| Method | Best for |
+| :--- | :--- |
+| Interactive shell (REPL) | Quick experiments, testing one expression |
+| Running a `.py` file | Real programs and scripts |
+| `python -c "code"` | A throwaway one-liner from the terminal |
+| IDE / editor Run button | Day-to-day development |
### Using the Command Line or Terminal
@@ -47,6 +67,29 @@ Once Python is installed, you can run Python code using the command line or term
4. **Run Your Python Script:**
Follow the steps mentioned above to navigate to the file's directory in the command line or terminal and run the script.
+:::tip
+**Naming `.py` files:** use lowercase letters and underscores (`hello_world.py`, `data_loader.py`). Avoid spaces, and never name a file after a standard library module (e.g. `random.py` or `math.py`) — doing so shadows the real module and causes confusing import errors.
+:::
+
+### Running a One-Liner
+
+For a quick test you do not even need a file. The `-c` flag runs a string of code directly:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python -c "print(2 ** 10)"
+1024
+```
+
+### Running a Module
+
+The `-m` flag runs an installed module as a script. This is how you start common tools:
+
+```bash title="command" showLineNumbers{1}
+python -m venv venv # create a virtual environment
+python -m pip install requests
+python -m http.server # start a quick local web server
+```
+
## Important Python Commands
### Interactive Mode
@@ -85,6 +128,10 @@ Hello, World!
C:\Users\Your Name>
```
+:::note
+You can also leave the shell with a keyboard shortcut: **`Ctrl + Z` then Enter** on Windows, or **`Ctrl + D`** on macOS/Linux. Both do the same thing as `exit()`.
+:::
+
### Running a Python File
- **Run a Python Script:**
diff --git a/src/content/docs/tutorials/Input.mdx b/src/content/docs/tutorials/Input.mdx
index 0fc84b1b..87dfd93a 100644
--- a/src/content/docs/tutorials/Input.mdx
+++ b/src/content/docs/tutorials/Input.mdx
@@ -15,6 +15,20 @@ User input is a crucial component of many Python programs, enabling interaction
The `input()` function is used to receive input from the user. It displays a prompt and waits for the user to enter data. The entered data is then returned as a string.
+**Diagram**:
+```mermaid title="how input() works" desc="The flow of reading user input"
+graph LR
+ A[Show prompt] --> B[Wait for user to type + press Enter]
+ B --> C[Return the text as a str]
+ C --> D{Need a number?}
+ D -->|Yes| E[Convert with int / float]
+ D -->|No| F[Use the string directly]
+```
+
+:::caution
+`input()` **always** returns a `str`, even if the user types digits. `input()` plus arithmetic without converting first leads to bugs: `"5" + "3"` is `"53"`, not `8`. Wrap it in `int()` or `float()` when you need a number.
+:::
+
The syntax of the `input()` function is as follows:
```python title="input.py" showLineNumbers{1}
@@ -109,6 +123,31 @@ Enter your height in meters: 1.75
In this example, `int()` and `float()` functions are used to convert the user input to integer and float data types, respectively.
+## Reading Multiple Values at Once
+
+To read several values from a single line, use `str.split()` to break the input on whitespace (or any separator). Combine it with `map()` to convert them all in one step.
+
+```python title="multi_input.py" showLineNumbers{1} {1-6}
+# User types: 3 7 1 9
+numbers = input("Enter numbers separated by spaces: ").split()
+print(numbers) # ['3', '7', '1', '9'] (strings)
+
+# Convert every item to int
+numbers = list(map(int, input("Enter numbers: ").split()))
+print(sum(numbers)) # 20
+```
+
+Unpacking works too, when you know exactly how many values to expect:
+
+```python title="unpack_input.py" showLineNumbers{1} {1-2}
+x, y = input("Enter two values: ").split(",") # split on a comma
+print(x, y)
+```
+
+:::tip
+User input often has stray spaces. Clean it with `.strip()` before using it: `name = input("Name: ").strip()`.
+:::
+
## Handling User Input Validation
Ensuring that the user provides valid input is crucial for the robustness of your program. Using conditional statements and exception handling, you can validate and handle potential errors gracefully:
diff --git a/src/content/docs/tutorials/Installation.mdx b/src/content/docs/tutorials/Installation.mdx
index 887c6f35..e15b9057 100644
--- a/src/content/docs/tutorials/Installation.mdx
+++ b/src/content/docs/tutorials/Installation.mdx
@@ -17,6 +17,14 @@ Visit the official Python website at [python.org](https://www.python.org/) to do
- **Linux:** Most Linux distributions come with Python pre-installed. If not, use your package manager to install Python.
+:::danger
+On **Windows**, the installer shows a checkbox labelled **"Add Python to PATH"** on the first screen. **Tick it** before clicking Install. Without it, the `python` and `pip` commands will not be recognised in the terminal, and you will see `'python' is not recognized as an internal or external command`.
+:::
+
+:::tip
+On macOS you can also install Python with [Homebrew](https://brew.sh/) (`brew install python`), and on Linux with your package manager (`sudo apt install python3` on Debian/Ubuntu). These keep Python easy to upgrade later.
+:::
+
## Step 2: Verify Installation
Once the installation is complete, it's essential to verify that Python has been installed correctly. Open a command prompt (or terminal) and enter the following command:
@@ -27,6 +35,32 @@ python --version
You should see the Python version number displayed, indicating a successful installation.
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python --version
+Python 3.12.2
+```
+
+:::note
+On Windows, if `python` does not work, try the **Python launcher** `py`:
+```cmd title="command" showLineNumbers{1}
+py --version
+```
+On macOS/Linux the command is often `python3` (and `pip3`) to avoid clashing with an old system Python 2.
+:::
+
+Also confirm that **pip**, Python's package installer, came along with it:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> pip --version
+pip 24.0 from C:\...\site-packages\pip (python 3.12)
+```
+
+It is good practice to upgrade pip right away:
+
+```bash title="command" showLineNumbers{1}
+python -m pip install --upgrade pip
+```
+
## Step 3: Install a Code Editor (Optional)
While Python can be written and run using a simple text editor, using a dedicated code editor can enhance your development experience. Choose a code editor that suits your preferences; popular choices include:
@@ -37,6 +71,16 @@ While Python can be written and run using a simple text editor, using a dedicate
## Step 4: Set Up a Virtual Environment (Optional but Recommended)
+A **virtual environment** is an isolated folder that holds its own copy of Python and its own installed packages. Each project gets its own, so upgrading a library for one project never breaks another. This is the single most important habit for a clean Python setup.
+
+**Diagram**:
+```mermaid title="why use a virtual environment" desc="Isolated dependencies per project"
+graph TD
+ A[Global Python] --> B[Project A venv: pandas 1.5]
+ A --> C[Project B venv: pandas 2.2]
+ A --> D[Project C venv: no extras]
+```
+
To manage project dependencies and ensure a clean development environment, consider setting up a virtual environment. In your project directory, run:
```bash title="command" showLineNumbers{1}
@@ -54,6 +98,18 @@ Activate the virtual environment:
source venv/bin/activate
```
+Once activated, your prompt shows `(venv)`. Install packages with pip, record them in a `requirements.txt`, and run `deactivate` when finished:
+
+```bash title="command" showLineNumbers{1}
+pip install requests
+pip freeze > requirements.txt # save the exact versions
+deactivate # leave the environment
+```
+
+:::tip
+Commit `requirements.txt` (not the `venv/` folder) to version control. Anyone can then recreate the exact environment with `pip install -r requirements.txt`.
+:::
+
## Step 5: Ready to Code!
With Python installed and your development environment set up, you're now ready to explore the tutorials and projects on Python Central Hub. Start with our [Introduction to Python](/tutorials/introduction/) tutorial to kick off your Python journey!
diff --git a/src/content/docs/tutorials/Introduction.mdx b/src/content/docs/tutorials/Introduction.mdx
index e0fcfeb1..4296d5ba 100644
--- a/src/content/docs/tutorials/Introduction.mdx
+++ b/src/content/docs/tutorials/Introduction.mdx
@@ -7,9 +7,55 @@ sidebar:
import DataCampExercise from "../../../components/DataCampExercise.astro";
+## What is Python?
+
+Python is a **high-level, interpreted, general-purpose** programming language. Each of those words means something concrete:
+
+- **High-level** — you work with human-friendly concepts (lists, dictionaries, objects) instead of memory addresses and registers.
+- **Interpreted** — code runs line by line through the Python interpreter; there is no separate compile step you manage by hand.
+- **General-purpose** — the same language powers websites, data pipelines, automation scripts, games, and AI models.
+- **Dynamically typed** — you do not declare variable types; Python figures them out at runtime.
+
+```python title="hello.py" showLineNumbers{1}
+print("Hello, World!")
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python hello.py
+Hello, World!
+```
+
+That single line is a complete Python program. The same idea in C or Java needs imports, a class, and a `main` function — Python's brevity is exactly why beginners and experts alike reach for it.
+
## History of Python
-Python, a high-level programming language known for its readability and versatility, was conceived by Guido van Rossum in the late 1980s. The first official Python release, Python 0.9.0, occurred in 1991. Over the years, Python has evolved through multiple versions, with Python 3 being the latest major release. The language's design philosophy prioritizes code readability and ease of use, making it a favorite among developers for various applications.
+Python, a high-level programming language known for its readability and versatility, was conceived by **Guido van Rossum** in the late 1980s at CWI in the Netherlands. He named it after the British comedy group *Monty Python*, not the snake. The first official release, Python 0.9.0, arrived in 1991. Over the years, Python has evolved through multiple versions, with Python 3 being the current major release. The language's design philosophy prioritizes code readability and ease of use, making it a favorite among developers for various applications.
+
+**Diagram**:
+```mermaid title="Python release timeline" desc="Major milestones in Python's history"
+graph LR
+ A[1991: Python 0.9.0] --> B[2000: Python 2.0]
+ B --> C[2008: Python 3.0]
+ C --> D[2020: Python 2 end-of-life]
+ D --> E[Today: Python 3.x]
+```
+
+### Python 2 vs. Python 3
+
+Python 2 reached **end-of-life in 2020** and no longer receives updates. All new projects should use Python 3. A few visible differences:
+
+| Feature | Python 2 | Python 3 |
+| :--- | :--- | :--- |
+| `print` | statement: `print "hi"` | function: `print("hi")` |
+| Division `5 / 2` | `2` (integer) | `2.5` (float) |
+| Strings | ASCII by default | Unicode by default |
+| Status | Dead (no support) | Actively developed |
+
+:::caution
+If you find old tutorials using `print "hello"` (no parentheses), they are Python 2 and will not run on a modern interpreter. Everything on Python Central Hub uses Python 3.
+:::
## Usage of Python
@@ -67,6 +113,54 @@ With libraries like Tkinter, Python enables the development of cross-platform de
Python's networking capabilities make it a preferred language for developing networking tools and applications.
+## How Python Code Runs
+
+When you run a Python file, the interpreter does not execute your text directly. It first compiles your source code into **bytecode** (a lower-level, platform-independent representation), then the **Python Virtual Machine (PVM)** executes that bytecode. This all happens automatically.
+
+**Diagram**:
+```mermaid title="Python execution model" desc="How source code becomes running output"
+graph LR
+ A[Source code: hello.py] --> B[Compiler]
+ B --> C[Bytecode: .pyc]
+ C --> D[Python Virtual Machine]
+ D --> E[Output]
+```
+
+The reference implementation of Python is called **CPython** (written in C). Other implementations exist — **PyPy** (faster, with a just-in-time compiler), **Jython** (runs on the Java JVM), and **MicroPython** (for microcontrollers).
+
+## The Interactive Shell (REPL)
+
+Beyond running files, Python ships with an interactive shell — the **REPL** (Read–Eval–Print Loop). Type `python` in your terminal and experiment line by line:
+
+```cmd title="command" showLineNumbers{1} {2,4,6}
+C:\Users\Your Name> python
+>>> 2 + 3
+5
+>>> name = "Python"
+>>> print("Hello,", name)
+Hello, Python
+>>> exit()
+```
+
+The REPL is perfect for testing a small idea before putting it in a file.
+
+## The Zen of Python
+
+Python's design philosophy is captured in a set of guiding aphorisms. Run `import this` to read them all:
+
+```python title="zen.py" showLineNumbers{1}
+import this
+```
+
+A few highlights:
+
+> Beautiful is better than ugly.
+> Explicit is better than implicit.
+> Simple is better than complex.
+> Readability counts.
+
+These principles explain *why* Python looks the way it does — and following them makes your own code more "Pythonic".
+
---
## Try it: Interactive Python Exercises
diff --git a/src/content/docs/tutorials/Modern Python/Concurrency.mdx b/src/content/docs/tutorials/Modern Python/Concurrency.mdx
new file mode 100644
index 00000000..d6e70cee
--- /dev/null
+++ b/src/content/docs/tutorials/Modern Python/Concurrency.mdx
@@ -0,0 +1,260 @@
+---
+title: Python Concurrency — threading, multiprocessing, asyncio
+description: A deep dive into Python concurrency. Learn the GIL, threading for I/O, multiprocessing for CPU work, and asyncio with async/await — when to use each, with examples and practice exercises.
+sidebar:
+ order: 2
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+Concurrency means making progress on **more than one task at a time**. Python offers three approaches, each suited to a different kind of work:
+
+| Approach | Best for | Why |
+|:---|:---|:---|
+| **threading** | I/O-bound work (network, disk) | Threads wait together; cheap to create. |
+| **multiprocessing** | CPU-bound work (number crunching) | Separate processes use multiple cores. |
+| **asyncio** | Many I/O tasks at once | One thread, cooperative `await`, very scalable. |
+
+## The GIL — why it matters
+
+CPython has a **Global Interpreter Lock (GIL)**: only one thread executes Python bytecode at a time. So threads **don't** speed up CPU-bound code — but they're great for I/O, where threads spend most time *waiting* (and release the GIL while waiting). For CPU-bound parallelism, use **multiprocessing**, which sidesteps the GIL with separate processes.
+
+> Rule of thumb: **I/O-bound → threading or asyncio. CPU-bound → multiprocessing.**
+
+## threading
+
+Run functions in separate threads. Start them, then `join` to wait for completion.
+
+```python title="threading_basic.py"
+import threading
+import time
+
+def worker(name):
+ print(f"{name} starting")
+ time.sleep(1) # simulates an I/O wait
+ print(f"{name} done")
+
+threads = [threading.Thread(target=worker, args=(f"T{i}",)) for i in range(3)]
+for t in threads:
+ t.start() # begin running concurrently
+for t in threads:
+ t.join() # wait for each to finish
+print("all done")
+```
+
+Because the threads sleep concurrently, three 1-second waits finish in about **1 second**, not 3.
+
+### Sharing data safely with a Lock
+
+When threads modify shared state, guard it with a `Lock` to avoid race conditions.
+
+```python title="lock.py"
+import threading
+
+counter = 0
+lock = threading.Lock()
+
+def increment():
+ global counter
+ for _ in range(100_000):
+ with lock: # only one thread at a time
+ counter += 1
+
+ts = [threading.Thread(target=increment) for _ in range(2)]
+for t in ts: t.start()
+for t in ts: t.join()
+print(counter) # 200000 (correct, thanks to the lock)
+```
+
+## concurrent.futures — a simpler interface
+
+`ThreadPoolExecutor` / `ProcessPoolExecutor` manage a pool of workers and collect results for you.
+
+```python title="executor.py"
+from concurrent.futures import ThreadPoolExecutor
+
+def square(n):
+ return n * n
+
+with ThreadPoolExecutor(max_workers=4) as pool:
+ results = list(pool.map(square, [1, 2, 3, 4, 5]))
+print(results) # [1, 4, 9, 16, 25]
+```
+
+Swap `ThreadPoolExecutor` for `ProcessPoolExecutor` to run CPU-bound work across cores — the API is identical.
+
+## multiprocessing
+
+Each process has its own Python interpreter and memory, so they run truly in parallel on multiple cores.
+
+```python title="multiprocessing_basic.py"
+from multiprocessing import Pool
+
+def heavy(n):
+ return sum(i * i for i in range(n))
+
+if __name__ == "__main__": # required guard on Windows/macOS
+ with Pool(processes=4) as pool:
+ results = pool.map(heavy, [10_000, 20_000, 30_000])
+ print(results)
+```
+
+> Always wrap multiprocessing entry code in `if __name__ == "__main__":` — without it, spawned processes re-import and re-run your script.
+
+## asyncio — async/await
+
+`asyncio` runs many I/O tasks on a **single thread** using cooperative multitasking. An `async def` function is a **coroutine**; `await` yields control while waiting, letting other coroutines run.
+
+```python title="asyncio_basic.py"
+import asyncio
+
+async def fetch(name, delay):
+ print(f"{name} start")
+ await asyncio.sleep(delay) # non-blocking wait
+ print(f"{name} done")
+ return name
+
+async def main():
+ # Run three coroutines concurrently
+ results = await asyncio.gather(
+ fetch("A", 1),
+ fetch("B", 1),
+ fetch("C", 1),
+ )
+ print(results)
+
+asyncio.run(main()) # entry point
+```
+
+All three "fetches" overlap, so the total time is about **1 second**, not 3.
+
+### Key asyncio pieces
+
+| Piece | Purpose |
+|:---|:---|
+| `async def` | Defines a coroutine. |
+| `await x` | Wait for an awaitable without blocking the thread. |
+| `asyncio.run(coro)` | Run the top-level coroutine. |
+| `asyncio.gather(*coros)` | Run many coroutines concurrently, collect results. |
+| `asyncio.create_task(coro)` | Schedule a coroutine to run in the background. |
+| `asyncio.sleep(s)` | Non-blocking sleep. |
+
+## Choosing the right tool
+
+```text title="decision.txt"
+Is the work CPU-bound (math, parsing, compression)?
+ -> multiprocessing (use all cores, beat the GIL)
+
+Is the work I/O-bound (HTTP, files, DB)?
+ A few tasks, simple code? -> threading / ThreadPoolExecutor
+ Hundreds/thousands of tasks? -> asyncio
+```
+
+## Common pitfalls
+
+- **Threads won't speed up CPU work** — the GIL serializes them; use multiprocessing.
+- **Forgetting `join`** — the main program may exit before threads finish.
+- **Unguarded shared state** — use a `Lock` around shared mutable data.
+- **Blocking calls inside asyncio** — `time.sleep` or heavy CPU work freezes the whole event loop; use `await asyncio.sleep` and run CPU work in an executor.
+- **Missing `if __name__ == "__main__":`** — breaks multiprocessing on Windows/macOS.
+
+## Practice Exercises
+
+### Exercise 1 – Run work in threads
+
+
+
+### Exercise 2 – Define and run a coroutine
+
+
+
+### Exercise 3 – Map work over a thread pool
+
+
+
+## Summary
+
+- The **GIL** means threads don't parallelize CPU work — they shine for I/O.
+- **threading** + `Lock` for simple concurrent I/O; **`concurrent.futures`** for pools and results.
+- **multiprocessing** for CPU-bound parallelism across cores (guard with `__main__`).
+- **asyncio** (`async`/`await`, `gather`, `run`) scales to many concurrent I/O tasks on one thread.
+- Match the tool to the workload: CPU → processes, I/O → threads/async.
diff --git a/src/content/docs/tutorials/Modern Python/Dataclasses.mdx b/src/content/docs/tutorials/Modern Python/Dataclasses.mdx
new file mode 100644
index 00000000..e532182d
--- /dev/null
+++ b/src/content/docs/tutorials/Modern Python/Dataclasses.mdx
@@ -0,0 +1,268 @@
+---
+title: Python Dataclasses (@dataclass)
+description: A deep dive into Python dataclasses. Learn how @dataclass auto-generates __init__, __repr__, and __eq__, plus defaults, default_factory, frozen and ordered classes, post-init, and asdict — with examples and practice exercises.
+sidebar:
+ order: 5
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+A **dataclass** is a class that mainly stores data. The `@dataclass` decorator (from the `dataclasses` module, Python 3.7+) writes the boilerplate for you: `__init__`, `__repr__`, `__eq__`, and more — generated from the field annotations.
+
+```python title="quickstart.py"
+from dataclasses import dataclass
+
+@dataclass
+class Point:
+ x: int
+ y: int
+
+p = Point(3, 4)
+print(p) # Point(x=3, y=4) <- free __repr__
+print(p.x, p.y) # 3 4
+print(p == Point(3, 4)) # True <- free __eq__
+```
+
+## What it generates for you
+
+Without dataclasses, that `Point` would need a hand-written `__init__`, `__repr__`, and `__eq__`. The decorator generates them from the annotated fields:
+
+| You write | `@dataclass` generates |
+|:---|:---|
+| `x: int` | A parameter in `__init__` and an attribute. |
+| (nothing) | `__repr__` → `Point(x=3, y=4)`. |
+| (nothing) | `__eq__` → field-by-field equality. |
+| `order=True` | `<`, `<=`, `>`, `>=` comparisons. |
+| `frozen=True` | Immutable, hashable instances. |
+
+## Default values
+
+Give fields defaults just like function parameters. Fields with defaults must come **after** those without.
+
+```python title="defaults.py"
+from dataclasses import dataclass
+
+@dataclass
+class User:
+ name: str
+ active: bool = True
+ role: str = "member"
+
+print(User("Ada")) # User(name='Ada', active=True, role='member')
+print(User("Bo", role="admin")) # User(name='Bo', active=True, role='admin')
+```
+
+## Mutable defaults — use default_factory
+
+You **cannot** use a mutable default (like `[]` or `{}`) directly — it would be shared across all instances. Use `field(default_factory=...)`.
+
+```python title="default_factory.py"
+from dataclasses import dataclass, field
+
+@dataclass
+class Cart:
+ items: list = field(default_factory=list) # a fresh list per instance
+ tags: dict = field(default_factory=dict)
+
+a = Cart()
+a.items.append("apple")
+b = Cart()
+print(a.items) # ['apple']
+print(b.items) # [] <- not shared, thanks to default_factory
+```
+
+> Writing `items: list = []` raises a `ValueError` in dataclasses precisely to prevent the shared-mutable-default bug.
+
+## Frozen (immutable) dataclasses
+
+`frozen=True` makes instances read-only and hashable — usable as dict keys or set members.
+
+```python title="frozen.py"
+from dataclasses import dataclass
+
+@dataclass(frozen=True)
+class Coord:
+ lat: float
+ lon: float
+
+c = Coord(51.5, -0.1)
+print(c) # Coord(lat=51.5, lon=-0.1)
+locations = {c: "London"} # works: frozen instances are hashable
+# c.lat = 0.0 # would raise FrozenInstanceError
+```
+
+## Ordered dataclasses
+
+`order=True` adds comparison operators based on the fields (compared as a tuple, top to bottom).
+
+```python title="order.py"
+from dataclasses import dataclass
+
+@dataclass(order=True)
+class Version:
+ major: int
+ minor: int
+
+versions = [Version(2, 0), Version(1, 5), Version(1, 9)]
+print(sorted(versions))
+# [Version(major=1, minor=5), Version(major=1, minor=9), Version(major=2, minor=0)]
+```
+
+## __post_init__ — validation and derived fields
+
+`__post_init__` runs right after the generated `__init__`, perfect for validation or computing derived values.
+
+```python title="post_init.py"
+from dataclasses import dataclass
+
+@dataclass
+class Rectangle:
+ width: float
+ height: float
+ area: float = 0.0
+
+ def __post_init__(self):
+ if self.width < 0 or self.height < 0:
+ raise ValueError("dimensions must be non-negative")
+ self.area = self.width * self.height
+
+r = Rectangle(3, 4)
+print(r.area) # 12.0
+```
+
+## Converting to dict or tuple
+
+```python title="asdict.py"
+from dataclasses import dataclass, asdict, astuple
+
+@dataclass
+class Point:
+ x: int
+ y: int
+
+p = Point(1, 2)
+print(asdict(p)) # {'x': 1, 'y': 2}
+print(astuple(p)) # (1, 2)
+```
+
+## When to use what
+
+| Need | Use |
+|:---|:---|
+| A mutable record with methods | **`@dataclass`** |
+| An immutable, hashable record | `@dataclass(frozen=True)` |
+| A tiny immutable record, tuple-like | `namedtuple` |
+| Arbitrary, schemaless data | a plain `dict` |
+
+## Common pitfalls
+
+- **Mutable defaults** — never `x: list = []`; use `field(default_factory=list)`.
+- **Field order** — non-default fields must precede default ones.
+- **Equality is field-based** — two instances with equal fields are `==` (usually what you want).
+- **`frozen=True` blocks attribute assignment** — set everything via `__init__`/`__post_init__`.
+
+## Practice Exercises
+
+### Exercise 1 – Define a dataclass
+
+
+
+### Exercise 2 – Safe mutable default
+
+
+
+### Exercise 3 – Convert to a dict
+
+
+
+## Summary
+
+- `@dataclass` auto-generates `__init__`, `__repr__`, and `__eq__` from field annotations.
+- Use defaults like function parameters; use `field(default_factory=...)` for mutable defaults.
+- `frozen=True` makes immutable, hashable records; `order=True` adds comparisons.
+- `__post_init__` validates or computes derived fields; `asdict`/`astuple` convert instances.
+- Reach for dataclasses whenever a class is mostly structured data.
diff --git a/src/content/docs/tutorials/Modern Python/F-Strings Deep Dive.mdx b/src/content/docs/tutorials/Modern Python/F-Strings Deep Dive.mdx
new file mode 100644
index 00000000..45fd3372
--- /dev/null
+++ b/src/content/docs/tutorials/Modern Python/F-Strings Deep Dive.mdx
@@ -0,0 +1,209 @@
+---
+title: Python f-strings Deep Dive
+description: An in-depth guide to Python f-strings — embedded expressions, the format spec mini-language (alignment, width, precision, thousands, number bases), the = debug specifier, conversions, and nesting. With examples and practice exercises.
+sidebar:
+ order: 7
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+**f-strings** (formatted string literals, PEP 498, Python 3.6+) are the fastest and most readable way to build strings. Prefix a string with `f` and embed expressions in `{}`. This page goes deep into formatting; for the broader picture of `%`, `.format()`, and templates, see [String Formatting](../../python-strings/formatting-string/).
+
+```python title="quickstart.py"
+name = "Ada"
+age = 36
+print(f"{name} is {age}") # Ada is 36
+print(f"Next year: {age + 1}") # Next year: 37 (any expression works)
+```
+
+## Expressions, not just variables
+
+Anything that evaluates to a value can go inside the braces — arithmetic, method calls, indexing, conditionals.
+
+```python title="expressions.py"
+items = ["a", "b", "c"]
+print(f"{len(items)} items") # 3 items
+print(f"upper: {'hi'.upper()}") # upper: HI
+print(f"first: {items[0]}") # first: a
+print(f"{'even' if 4 % 2 == 0 else 'odd'}") # even
+```
+
+## The format spec mini-language
+
+After a colon inside the braces comes a **format spec** that controls width, alignment, precision, and type. The general shape is:
+
+```text title="spec.txt"
+{value:[fill][align][sign][width][,][.precision][type]}
+```
+
+### Number precision
+
+```python title="precision.py"
+pi = 3.14159265
+print(f"{pi:.2f}") # 3.14 (2 decimal places)
+print(f"{pi:.0f}") # 3 (no decimals)
+print(f"{1/3:.4f}") # 0.3333
+```
+
+### Width, alignment, and fill
+
+| Spec | Meaning |
+|:---|:---|
+| `:<10` | Left-align in width 10. |
+| `:>10` | Right-align in width 10. |
+| `:^10` | Center in width 10. |
+| `:*^10` | Center, padding with `*`. |
+| `:08` | Zero-pad to width 8. |
+
+```python title="align.py"
+print(f"{'hi':<6}|") # hi |
+print(f"{'hi':>6}|") # | hi -> right aligned
+print(f"{'hi':^6}|") # | hi |
+print(f"{42:08}") # 00000042
+print(f"{'x':*^7}") # ***x***
+```
+
+### Thousands separators and percent
+
+```python title="numbers.py"
+print(f"{1000000:,}") # 1,000,000
+print(f"{1234.5:,.2f}") # 1,234.50
+print(f"{0.1234:.1%}") # 12.3% (percent with 1 decimal)
+print(f"{255:#x}") # 0xff (hex with prefix)
+```
+
+### Number bases
+
+| Type | Base |
+|:---|:---|
+| `:b` | Binary |
+| `:o` | Octal |
+| `:x` / `:X` | Hex (lower/upper) |
+| `:e` | Scientific notation |
+
+```python title="bases.py"
+n = 255
+print(f"{n:b}") # 11111111
+print(f"{n:o}") # 377
+print(f"{n:x}") # ff
+print(f"{12345.678:.2e}") # 1.23e+04
+```
+
+## The `=` debug specifier (3.8+)
+
+Add `=` inside the braces to print **both the expression and its value** — perfect for quick debugging.
+
+```python title="debug.py"
+x = 10
+y = 3
+print(f"{x=}") # x=10
+print(f"{x + y=}") # x + y=13
+print(f"{x / y=:.2f}") # x / y=3.33 (combine with a format spec)
+```
+
+## Conversions: !r, !s, !a
+
+Apply `repr()`, `str()`, or `ascii()` before formatting with `!r`, `!s`, `!a`.
+
+```python title="conversions.py"
+name = "Ada"
+print(f"{name!r}") # 'Ada' (repr -> shows quotes)
+print(f"{name!s}") # Ada (str)
+```
+
+## Nesting and dynamic specs
+
+Braces can contain expressions that themselves come from variables — even the format spec.
+
+```python title="nesting.py"
+value = 3.14159
+places = 3
+print(f"{value:.{places}f}") # 3.142 (precision taken from a variable)
+
+width = 10
+print(f"{'hi':>{width}}") # right-align in a width from a variable
+```
+
+## Multiline f-strings and escaping braces
+
+```python title="multiline_escape.py"
+name, role = "Ada", "admin"
+msg = (
+ f"User: {name}\n"
+ f"Role: {role}"
+)
+print(msg)
+
+# To print literal braces, double them
+print(f"{{not a placeholder}} but {name}") # {not a placeholder} but Ada
+```
+
+## Common pitfalls
+
+- **Forgetting the `f`** — `"{x}"` prints the braces literally; `f"{x}"` substitutes.
+- **Literal braces need doubling** — `{{` and `}}` to print `{` and `}`.
+- **Quote clashes** — don't reuse the outer quote inside (or use the other quote): `f"{d['key']}"`.
+- **Heavy logic in braces hurts readability** — compute first, then format.
+
+## Practice Exercises
+
+### Exercise 1 – Two decimal places
+
+
+
+### Exercise 2 – Thousands separator
+
+
+
+### Exercise 3 – The debug specifier
+
+
+
+## Summary
+
+- f-strings embed any expression in `{}` and are the fastest, clearest way to format strings.
+- The format spec (`:[fill][align][sign][width][,][.precision][type]`) controls precision, padding, separators, and number bases.
+- `{expr=}` prints the expression and its value — handy for debugging.
+- Use `!r`/`!s`/`!a` for conversions, nest `{...}` for dynamic specs, and double `{{ }}` for literal braces.
diff --git a/src/content/docs/tutorials/Modern Python/Networking.mdx b/src/content/docs/tutorials/Modern Python/Networking.mdx
new file mode 100644
index 00000000..f661305f
--- /dev/null
+++ b/src/content/docs/tutorials/Modern Python/Networking.mdx
@@ -0,0 +1,224 @@
+---
+title: Python Networking — requests, urllib & sockets
+description: A practical guide to networking in Python. Make HTTP calls with requests and urllib, build and parse URLs with urllib.parse, and understand low-level TCP sockets. With examples and practice exercises.
+sidebar:
+ order: 4
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+Networking in Python spans two levels: **high-level HTTP** (talking to web APIs and sites) and **low-level sockets** (raw TCP/UDP connections). Most apps live at the HTTP level.
+
+| Tool | Level | Notes |
+|:---|:---|:---|
+| `requests` | High (HTTP) | Third-party, the de-facto standard. `pip install requests`. |
+| `urllib.request` | High (HTTP) | Built in; more verbose, no install needed. |
+| `urllib.parse` | Helper | Build and parse URLs (pure, no network). |
+| `socket` | Low (TCP/UDP) | Raw connections; build your own protocols. |
+
+## requests — the friendly HTTP client
+
+`requests` makes HTTP calls readable. Install it first (`pip install requests`).
+
+```python title="requests_get.py"
+import requests
+
+resp = requests.get("https://api.example.com/users", params={"page": 2})
+print(resp.status_code) # 200
+print(resp.headers["Content-Type"])
+data = resp.json() # parse a JSON body into Python
+print(data)
+```
+
+```python title="requests_post.py"
+import requests
+
+# Send JSON in the body
+resp = requests.post(
+ "https://api.example.com/users",
+ json={"name": "Ada", "role": "admin"},
+ headers={"Authorization": "Bearer TOKEN"},
+ timeout=10,
+)
+resp.raise_for_status() # raise if status is 4xx/5xx
+print(resp.json())
+```
+
+| `requests` piece | Purpose |
+|:---|:---|
+| `requests.get/post/put/delete` | The HTTP verbs. |
+| `params={...}` | Query-string parameters. |
+| `json={...}` | Send a JSON body. |
+| `headers={...}` | Custom request headers. |
+| `resp.status_code` | The HTTP status (200, 404, ...). |
+| `resp.json()` / `resp.text` | Parsed JSON / raw text. |
+| `resp.raise_for_status()` | Turn error statuses into exceptions. |
+| `timeout=` | Fail fast instead of hanging forever. |
+
+## urllib — the standard-library option
+
+When you can't add dependencies, `urllib.request` does the same job with more ceremony.
+
+```python title="urllib_get.py"
+import urllib.request
+import json
+
+req = urllib.request.Request(
+ "https://api.example.com/data",
+ headers={"User-Agent": "my-app"},
+)
+with urllib.request.urlopen(req, timeout=10) as resp:
+ body = resp.read().decode("utf-8")
+ data = json.loads(body)
+print(data)
+```
+
+## urllib.parse — build and parse URLs
+
+This part of `urllib` is pure (no network) and extremely useful for assembling query strings and dissecting URLs.
+
+```python title="urllib_parse.py"
+from urllib.parse import urlencode, urlparse, parse_qs
+
+# Build a query string from a dict
+qs = urlencode({"q": "python", "page": 2})
+print(qs) # q=python&page=2
+
+# Dissect a URL
+parts = urlparse("https://example.com/search?q=python&page=2")
+print(parts.scheme) # https
+print(parts.netloc) # example.com
+print(parts.path) # /search
+print(parse_qs(parts.query)) # {'q': ['python'], 'page': ['2']}
+```
+
+## socket — low-level TCP
+
+For custom protocols or learning how the network really works, use raw sockets. Here's a minimal echo server and client.
+
+```python title="tcp_server.py"
+import socket
+
+server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+server.bind(("127.0.0.1", 9000))
+server.listen()
+print("listening on 9000")
+
+conn, addr = server.accept() # blocks until a client connects
+with conn:
+ data = conn.recv(1024) # read up to 1024 bytes
+ conn.sendall(data) # echo it back
+```
+
+```python title="tcp_client.py"
+import socket
+
+with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+ s.connect(("127.0.0.1", 9000))
+ s.sendall(b"hello")
+ reply = s.recv(1024)
+print(reply) # b'hello'
+```
+
+| socket call | Purpose |
+|:---|:---|
+| `socket(AF_INET, SOCK_STREAM)` | Create a TCP socket. |
+| `bind((host, port))` | Attach a server to an address. |
+| `listen()` / `accept()` | Wait for and accept a connection. |
+| `connect((host, port))` | Client connects to a server. |
+| `sendall(bytes)` / `recv(n)` | Send / receive raw bytes. |
+
+> Sockets speak **bytes**, not strings — encode with `.encode()` and decode with `.decode()`.
+
+## Choosing a level
+
+```text title="decision.txt"
+Talking to a web API or website? -> requests (or urllib if no installs)
+Just building/parsing a URL? -> urllib.parse
+Custom protocol / raw TCP/UDP? -> socket
+```
+
+## Common pitfalls
+
+- **No timeout** — network calls can hang forever; always pass `timeout=`.
+- **Ignoring status codes** — check `resp.status_code` or call `raise_for_status()`.
+- **Sending strings over sockets** — sockets need bytes; encode/decode explicitly.
+- **`requests` isn't built in** — it needs `pip install requests`; `urllib` does not.
+
+## Practice Exercises
+
+These exercises use `urllib.parse`, which runs without any network access.
+
+### Exercise 1 – Build a query string
+
+
+
+### Exercise 2 – Extract the host from a URL
+
+
+
+### Exercise 3 – Parse query parameters
+
+
+
+## Summary
+
+- For HTTP, reach for **`requests`** (clean API) or **`urllib.request`** (built in, verbose).
+- Always set a **`timeout`** and check the **status code**.
+- **`urllib.parse`** builds and dissects URLs with `urlencode`, `urlparse`, and `parse_qs`.
+- **`socket`** gives raw TCP/UDP for custom protocols — remember it speaks bytes.
+- Pick the level that matches the task: APIs → requests, URLs → urllib.parse, raw connections → socket.
diff --git a/src/content/docs/tutorials/Modern Python/Type Hints and typing.mdx b/src/content/docs/tutorials/Modern Python/Type Hints and typing.mdx
new file mode 100644
index 00000000..8d71ab63
--- /dev/null
+++ b/src/content/docs/tutorials/Modern Python/Type Hints and typing.mdx
@@ -0,0 +1,286 @@
+---
+title: Python Type Hints & the typing Module
+description: A deep dive into Python type hints — List, Dict, Optional, Union, the modern | syntax, Callable, TypeVar generics, type aliases, and static checking with mypy. Includes examples and practice exercises.
+sidebar:
+ order: 1
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+**Type hints** annotate the expected types of variables, parameters, and return values. They don't change how your code runs — Python ignores them at runtime — but they make code self-documenting and let tools like **mypy** catch bugs *before* you run anything. This page goes well beyond basic annotations (see also the [Function Annotations](../../python-function/function-annotations/) page for the basics).
+
+```python title="quickstart.py"
+def greet(name: str, excited: bool = False) -> str:
+ msg = f"Hello, {name}"
+ return msg + "!" if excited else msg
+
+age: int = 30
+pi: float = 3.14159
+names: list[str] = ["Ada", "Bob"]
+```
+
+## Why bother?
+
+- **Documentation that can't drift** — the signature states the contract.
+- **Editor support** — autocomplete and inline errors.
+- **Static checking** — `mypy` flags type mismatches without running the code.
+- **Safer refactors** — change a type, and the checker shows every break.
+
+## Built-in collection generics
+
+Since Python 3.9 you can subscript the built-in containers directly. (Older code imports `List`, `Dict`, etc. from `typing`.)
+
+| Modern (3.9+) | Legacy (`from typing import ...`) | Means |
+|:---|:---|:---|
+| `list[int]` | `List[int]` | A list of ints. |
+| `dict[str, int]` | `Dict[str, int]` | A dict: str keys, int values. |
+| `tuple[int, str]` | `Tuple[int, str]` | A 2-tuple. |
+| `set[str]` | `Set[str]` | A set of strings. |
+| `tuple[int, ...]` | `Tuple[int, ...]` | A tuple of any length of ints. |
+
+```python title="collections.py"
+def total(prices: list[float]) -> float:
+ return sum(prices)
+
+def counts(words: list[str]) -> dict[str, int]:
+ result: dict[str, int] = {}
+ for w in words:
+ result[w] = result.get(w, 0) + 1
+ return result
+```
+
+## Optional and Union
+
+A value that may be `None` is **Optional**. A value that may be one of several types is a **Union**.
+
+```python title="optional_union.py"
+from typing import Optional, Union
+
+# Optional[str] means "str or None"
+def find_user(uid: int) -> Optional[str]:
+ users = {1: "Ada"}
+ return users.get(uid) # returns str or None
+
+# Union means "any of these types"
+def to_int(x: Union[int, str]) -> int:
+ return int(x)
+```
+
+Since Python 3.10 you can use the cleaner **`|`** syntax:
+
+```python title="pipe_union.py"
+def find_user(uid: int) -> str | None: # same as Optional[str]
+ ...
+
+def to_int(x: int | str) -> int: # same as Union[int, str]
+ return int(x)
+```
+
+> `Optional[X]` is exactly `X | None`. It does **not** mean the argument is optional — it means the *value* can be `None`.
+
+## Any, and when to avoid it
+
+`Any` disables type checking for that value — use it sparingly, as it defeats the purpose.
+
+```python title="any.py"
+from typing import Any
+
+def debug(value: Any) -> None: # accepts literally anything
+ print(repr(value))
+```
+
+## Callable — typing functions
+
+`Callable[[ArgTypes], ReturnType]` describes a function passed as a value.
+
+```python title="callable.py"
+from typing import Callable
+
+def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
+ return func(a, b)
+
+print(apply(lambda x, y: x + y, 3, 4)) # 7
+```
+
+## Type aliases
+
+Give a complex type a readable name.
+
+```python title="aliases.py"
+from typing import Union
+
+# A reusable alias
+Number = Union[int, float]
+Matrix = list[list[float]]
+
+def scale(m: Matrix, factor: Number) -> Matrix:
+ return [[cell * factor for cell in row] for row in m]
+```
+
+## Generics with TypeVar
+
+A **TypeVar** lets you write functions and classes that work for *any* type while preserving the relationship between input and output.
+
+```python title="generics.py"
+from typing import TypeVar
+
+T = TypeVar("T")
+
+def first(items: list[T]) -> T: # returns the SAME type the list holds
+ return items[0]
+
+x: int = first([1, 2, 3]) # checker knows x is int
+y: str = first(["a", "b"]) # checker knows y is str
+```
+
+Generic **classes** subclass `Generic[T]`:
+
+```python title="generic_class.py"
+from typing import Generic, TypeVar
+
+T = TypeVar("T")
+
+class Box(Generic[T]):
+ def __init__(self, item: T) -> None:
+ self.item = item
+ def get(self) -> T:
+ return self.item
+
+b: Box[int] = Box(42)
+print(b.get()) # 42
+```
+
+## Other handy typing tools
+
+| Tool | Use |
+|:---|:---|
+| `Literal["a", "b"]` | Restrict to specific literal values. |
+| `Final` | Mark a constant that shouldn't be reassigned. |
+| `TypedDict` | A dict with a fixed set of typed keys. |
+| `Sequence` / `Iterable` / `Mapping` | Accept any sequence/iterable/mapping, not just `list`/`dict`. |
+
+```python title="abc_types.py"
+from typing import Iterable
+
+# Accept ANY iterable of ints (list, tuple, generator, ...)
+def total(nums: Iterable[int]) -> int:
+ return sum(nums)
+
+print(total([1, 2, 3])) # 6
+print(total((4, 5))) # 9
+```
+
+## Checking types with mypy
+
+Hints are not enforced at runtime — run a checker to catch mistakes.
+
+```bash title="terminal"
+$ pip install mypy
+$ mypy myscript.py
+myscript.py:3: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
+```
+
+## Common pitfalls
+
+- **Hints don't enforce anything at runtime** — `def f(x: int)` still accepts a string unless a checker complains.
+- **`Optional[X]` is about `None`**, not about omittable arguments (that's a default value).
+- **Use `Sequence`/`Iterable` for parameters**, concrete types for return values — accept broadly, return precisely.
+- **Don't overuse `Any`** — it silences the checker.
+
+## Practice Exercises
+
+### Exercise 1 – Annotate a function
+
+ int:
+ return a + b
+
+print(add(2, 5))
+
+# Expected Output:
+# 7`}
+ solution={`def add(a: int, b: int) -> int:
+ return a + b
+
+print(add(2, 5))`}
+ sct={`test_output_contains("7")
+success_msg("Type hints document the contract without changing behaviour.")`}
+ height={160}
+/>
+
+### Exercise 2 – Optional return
+
+ ___[str]:
+ table = {"a": "apple"}
+ return table.get(key)
+
+print(lookup("a"))
+print(lookup("z"))
+
+# Expected Output:
+# apple
+# None`}
+ solution={`from typing import Optional
+
+def lookup(key: str) -> Optional[str]:
+ table = {"a": "apple"}
+ return table.get(key)
+
+print(lookup("a"))
+print(lookup("z"))`}
+ sct={`test_output_contains("apple")
+test_output_contains("None")
+success_msg("Optional[str] == str | None.")`}
+ height={200}
+/>
+
+### Exercise 3 – A generic 'first' function
+
+ T.`}
+ code={`# Task: Make 'first' generic so it returns the element type.
+from typing import TypeVar
+
+T = TypeVar("___")
+
+def first(items: list[T]) -> T:
+ return items[0]
+
+print(first([10, 20, 30]))
+print(first(["x", "y"]))
+
+# Expected Output:
+# 10
+# x`}
+ solution={`from typing import TypeVar
+
+T = TypeVar("T")
+
+def first(items: list[T]) -> T:
+ return items[0]
+
+print(first([10, 20, 30]))
+print(first(["x", "y"]))`}
+ sct={`test_output_contains("10")
+test_output_contains("x")
+success_msg("A TypeVar preserves the relationship between input and output types.")`}
+ height={210}
+/>
+
+## Summary
+
+- Type hints document expected types; they're ignored at runtime but power editors and `mypy`.
+- Use built-in generics (`list[int]`, `dict[str, int]`) on 3.9+, or `typing` equivalents on older versions.
+- `Optional[X]` / `X | None` for nullable values; `Union` / `A | B` for multiple types.
+- Type functions with `Callable`, reuse complex types with aliases, and write reusable code with `TypeVar`/`Generic`.
+- Prefer `Iterable`/`Sequence` for inputs; run `mypy` to enforce.
diff --git a/src/content/docs/tutorials/Modern Python/Virtual Environments and pip.mdx b/src/content/docs/tutorials/Modern Python/Virtual Environments and pip.mdx
new file mode 100644
index 00000000..f5524245
--- /dev/null
+++ b/src/content/docs/tutorials/Modern Python/Virtual Environments and pip.mdx
@@ -0,0 +1,186 @@
+---
+title: Python Virtual Environments & pip
+description: Learn to isolate project dependencies with venv and manage packages with pip — creating environments, activating them, installing/freezing/uninstalling packages, and requirements.txt. With examples and practice exercises.
+sidebar:
+ order: 3
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+A **virtual environment** is an isolated Python installation for a single project. It keeps each project's packages separate so versions never clash, and it keeps your global Python clean. **pip** is the tool that installs and manages those packages. (For installing Python itself, see the [Installation](../../installation/) page.)
+
+> Golden rule: **one virtual environment per project**, and never `pip install` into your system Python.
+
+## Why isolate?
+
+Without isolation, every project shares one set of packages:
+
+- Project A needs `django==3.2`, Project B needs `django==4.2` — they can't coexist globally.
+- A global install can break system tools that rely on specific versions.
+- You can't reproduce "exactly what this project needs" for a teammate or server.
+
+A virtual environment solves all three.
+
+## Creating an environment with venv
+
+`venv` ships with Python. Create an environment (commonly named `.venv`) in your project folder:
+
+```bash title="terminal"
+# Create a virtual environment in the .venv folder
+python -m venv .venv
+```
+
+This makes a `.venv/` directory containing a private copy of Python and a place for packages.
+
+## Activating and deactivating
+
+You must **activate** the environment so `python` and `pip` point at it.
+
+```bash title="macOS / Linux"
+source .venv/bin/activate
+```
+
+```powershell title="Windows (PowerShell)"
+.venv\Scripts\Activate.ps1
+```
+
+Once active, your prompt usually shows `(.venv)`. To leave it:
+
+```bash title="terminal"
+deactivate
+```
+
+> Tip: add `.venv/` to your `.gitignore` — environments are rebuilt from `requirements.txt`, not committed.
+
+## Installing packages with pip
+
+With the environment active:
+
+```bash title="terminal"
+pip install requests # latest version
+pip install "django==4.2" # a specific version
+pip install "flask>=2,<3" # a version range
+pip install --upgrade requests # upgrade to the newest
+pip uninstall requests # remove a package
+```
+
+| Command | Does |
+|:---|:---|
+| `pip install NAME` | Install the latest version. |
+| `pip install NAME==X.Y` | Install an exact version. |
+| `pip install --upgrade NAME` | Upgrade a package. |
+| `pip uninstall NAME` | Remove a package. |
+| `pip list` | List installed packages. |
+| `pip show NAME` | Show details about a package. |
+
+## requirements.txt — reproducible installs
+
+Record exact versions so anyone can recreate the environment.
+
+```bash title="terminal"
+# Save the current environment's packages
+pip freeze > requirements.txt
+
+# Recreate it elsewhere (in a fresh venv)
+pip install -r requirements.txt
+```
+
+A `requirements.txt` looks like:
+
+```text title="requirements.txt"
+requests==2.31.0
+flask==3.0.0
+python-dotenv==1.0.0
+```
+
+## A typical project workflow
+
+```bash title="terminal"
+mkdir myproject && cd myproject
+python -m venv .venv
+source .venv/bin/activate # (Windows: .venv\Scripts\Activate.ps1)
+pip install requests flask
+pip freeze > requirements.txt
+# ... write code ...
+deactivate
+```
+
+## Common pitfalls
+
+- **Forgetting to activate** — `pip install` then lands in global Python. Check your prompt for `(.venv)`.
+- **Committing `.venv/`** — it's large and machine-specific; commit `requirements.txt` instead.
+- **Mixing `pip` and the system package manager** — inside a venv, always use `pip`.
+- **`python` vs `python3`** — on some systems the command is `python3`; the same goes for `pip`/`pip3`.
+
+## Practice Exercises
+
+These run plain Python that mirrors what the tools do under the hood.
+
+### Exercise 1 – Check the running Python version
+
+
+
+### Exercise 2 – Parse a requirements line
+
+
+
+### Exercise 3 – Build a pip install command
+
+
+
+## Summary
+
+- A **virtual environment** isolates one project's packages; create it with `python -m venv .venv`.
+- **Activate** it before working (`source .venv/bin/activate` or `Activate.ps1`), `deactivate` to leave.
+- Use **pip** to `install`, `upgrade`, `uninstall`, and `list` packages.
+- Pin versions with `pip freeze > requirements.txt` and restore with `pip install -r requirements.txt`.
+- Commit `requirements.txt`, not the `.venv/` folder.
diff --git a/src/content/docs/tutorials/Modern Python/Walrus Operator.mdx b/src/content/docs/tutorials/Modern Python/Walrus Operator.mdx
new file mode 100644
index 00000000..7f857884
--- /dev/null
+++ b/src/content/docs/tutorials/Modern Python/Walrus Operator.mdx
@@ -0,0 +1,170 @@
+---
+title: Python Walrus Operator (:=)
+description: A focused guide to Python's walrus operator (:=), the assignment expression introduced in 3.8. Learn where it shines — while loops, if statements, and comprehensions — plus pitfalls and practice exercises.
+sidebar:
+ order: 6
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+The **walrus operator** `:=` (introduced in Python 3.8, PEP 572) is an **assignment expression**: it assigns a value to a variable **and** returns that value, all in one expression. The name comes from its resemblance to a walrus's eyes and tusks.
+
+```python title="quickstart.py"
+# Without walrus: compute, store, then test
+n = len("hello")
+if n > 3:
+ print(f"{n} characters")
+
+# With walrus: assign and test in one line
+if (n := len("hello")) > 3:
+ print(f"{n} characters")
+# 5 characters
+```
+
+## Assignment statement vs assignment expression
+
+| `=` (statement) | `:=` (expression) |
+|:---|:---|
+| `x = 5` stands alone. | `(x := 5)` can sit inside a larger expression. |
+| Returns nothing. | Evaluates to the assigned value. |
+| Can't be used in `if`/`while` conditions. | Designed exactly for that. |
+
+> Wrap walrus expressions in parentheses in most contexts — it keeps intent clear and is required in several places.
+
+## Where it shines
+
+### 1. While loops that read-and-test
+
+Avoid duplicating the "read" both before and inside the loop.
+
+```python title="while_loop.py"
+# Process items from an iterator until a sentinel (0) appears
+data = [5, 8, 3, 0, 9]
+it = iter(data)
+
+while (value := next(it, 0)) != 0:
+ print(value)
+# 5
+# 8
+# 3
+```
+
+A classic real-world shape is reading chunks from a file or stream:
+
+```python title="read_chunks.py"
+# Pseudocode pattern — read until there's nothing left
+# while (chunk := file.read(1024)):
+# process(chunk)
+```
+
+### 2. if statements that capture a result
+
+Compute once, test, then reuse the captured value in the body.
+
+```python title="if_capture.py"
+text = "hello world"
+
+if (count := text.count("o")) > 1:
+ print(f"Found 'o' {count} times")
+# Found 'o' 2 times
+```
+
+### 3. Comprehensions without recomputing
+
+Compute an expensive value once and both filter and keep it.
+
+```python title="comprehension.py"
+numbers = [1, 2, 3, 4, 5, 6]
+
+# Keep doubled values that exceed 5 — double() is computed once per item
+result = [doubled for n in numbers if (doubled := n * 2) > 5]
+print(result) # [6, 8, 10, 12]
+```
+
+Without the walrus you'd compute `n * 2` twice (once to filter, once to keep) or need a longer loop.
+
+## Common pitfalls
+
+- **It's not a plain `=`** — you can't write `(x := 5)` to replace a normal top-level assignment; just use `x = 5` there.
+- **Readability** — overusing `:=` makes lines dense. Reach for it when it removes duplication, not to win at code golf.
+- **Parentheses** — `n := len(s)` alone is a syntax error in many spots; write `(n := len(s))`.
+- **Python 3.8+ only** — older interpreters reject `:=`.
+
+## Practice Exercises
+
+### Exercise 1 – Capture a length in an if
+
+ 3.
+word = "python"
+
+if (n ___ len(word)) > 3:
+ print(n)
+
+# Expected Output:
+# 6`}
+ solution={`word = "python"
+
+if (n := len(word)) > 3:
+ print(n)`}
+ sct={`test_output_contains("6")
+success_msg("The walrus assigned len(word) to n and tested it at once.")`}
+ height={150}
+/>
+
+### Exercise 2 – Filter in a comprehension
+
+ 4]
+print(result)
+
+# Expected Output:
+# [9, 16]`}
+ solution={`numbers = [1, 2, 3, 4]
+
+result = [sq for n in numbers if (sq := n * n) > 4]
+print(result)`}
+ sct={`test_output_contains("[9, 16]")
+success_msg("The walrus computes n*n once and reuses it.")`}
+ height={160}
+/>
+
+### Exercise 3 – Loop until a sentinel
+
+
+
+## Summary
+
+- `:=` assigns **and** returns a value, so it works inside expressions.
+- Great for `while` loops (read-and-test), `if` statements (capture-and-test), and comprehensions (compute once).
+- Always parenthesize it; use it to remove duplication, not to obscure code.
+- Requires Python 3.8+.
diff --git a/src/content/docs/tutorials/Numbers.mdx b/src/content/docs/tutorials/Numbers.mdx
index 3f6b41eb..6048e276 100644
--- a/src/content/docs/tutorials/Numbers.mdx
+++ b/src/content/docs/tutorials/Numbers.mdx
@@ -24,6 +24,33 @@ result = a + b # Result is -5
Python allows unlimited precision for integers, meaning they can be as large as your system's memory allows.
+```python title="bignum.py" showLineNumbers{1} {1-2}
+big = 2 ** 100 # no overflow, Python handles huge integers
+print(big) # 1267650600228229401496703205376
+```
+
+### Number Bases and Readable Literals
+
+Integers can be written in binary, octal, and hexadecimal using prefixes, and you can group digits with underscores for readability (the underscores are ignored by Python):
+
+```python title="bases.py" showLineNumbers{1} {1-8}
+binary = 0b1010 # binary -> 10
+octal = 0o17 # octal -> 15
+hexadecimal = 0xFF # hex -> 255
+million = 1_000_000 # underscores aid reading -> 1000000
+
+print(binary, octal, hexadecimal, million)
+print(bin(10), oct(15), hex(255)) # convert back to base strings
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-3}
+C:\Users\Your Name> python bases.py
+10 15 255 1000000
+0b1010 0o17 0xff
+```
+
## Floating-Point Numbers
#### `float`
@@ -37,6 +64,28 @@ area = pi * (radius ** 2) # Calculating the area of a circle
While floating-point numbers are powerful for scientific and mathematical computations, they may introduce precision issues due to the binary representation of real numbers.
+### The Floating-Point Precision Gotcha
+
+This surprises every beginner — floats are stored in binary, so some decimals cannot be represented exactly:
+
+```python title="precision.py" showLineNumbers{1} {1-2}
+print(0.1 + 0.2) # 0.30000000000000004
+print(0.1 + 0.2 == 0.3) # False!
+```
+
+When exact decimal arithmetic matters (money, for example), use the `decimal` module:
+
+```python title="decimal_money.py" showLineNumbers{1} {1-4}
+from decimal import Decimal
+
+price = Decimal("0.1") + Decimal("0.2")
+print(price) # 0.3 (exact)
+```
+
+:::tip
+To compare floats safely, do not use `==`. Use `math.isclose(a, b)` to check whether they are *approximately* equal.
+:::
+
## Complex Numbers
#### `complex`
@@ -125,10 +174,10 @@ import math
sqrt_result = math.sqrt(25) # Result is 5.0
```
:::caution
-The `math` module only works with floating-point numbers. If you want to perform mathematical operations on integers, use the `cmath` module instead.
+`math` functions accept integers and floats but **always return a float** (`math.sqrt(25)` is `5.0`, not `5`). For complex numbers, `math` raises an error — use the `cmath` module instead, which is built specifically for complex math.
:::
:::note
-For more information on the `math` module, check out our [Python Math Module Tutorial](https://docs.python.org/3/library/math.html).
+For more information, see the official [`math` module documentation](https://docs.python.org/3/library/math.html). Useful members include `math.pi`, `math.e`, `math.floor()`, `math.ceil()`, `math.factorial()`, and `math.gcd()`.
:::
## Random Numbers
@@ -143,7 +192,30 @@ print(random.randrange(1, 10)) # Prints a random integer between 1 and 10
For more information on the `random` module, check out our [Python Random Module Tutorial](https://docs.python.org/3/library/random.html).
:::
+## Built-in Number Functions
+You do not always need the `math` module. Python has several handy built-ins for numbers:
+
+```python title="builtins.py" showLineNumbers{1} {1-6}
+print(abs(-7)) # 7 -> absolute value
+print(round(3.14159, 2)) # 3.14 -> round to 2 decimals
+print(pow(2, 8)) # 256 -> 2 to the power 8
+print(divmod(17, 5)) # (3, 2) -> quotient and remainder together
+print(max(3, 9, 1)) # 9
+print(min(3, 9, 1)) # 1
+```
+
+## Augmented Assignment
+
+Operators like `+=`, `-=`, `*=`, `/=`, `//=`, `%=`, and `**=` update a variable in place — a shorthand that keeps code concise:
+
+```python title="augmented.py" showLineNumbers{1} {1-5}
+count = 10
+count += 5 # same as: count = count + 5
+count *= 2 # same as: count = count * 2
+count //= 3 # floor division assignment
+print(count) # 10
+```
## Conclusion
diff --git a/src/content/docs/tutorials/Print.mdx b/src/content/docs/tutorials/Print.mdx
index 0eb49c63..65d71d0c 100644
--- a/src/content/docs/tutorials/Print.mdx
+++ b/src/content/docs/tutorials/Print.mdx
@@ -62,6 +62,29 @@ C:\Users\Your Name> python print.py
Bob is 30 years old.
```
+#### F-String Format Specifiers
+
+F-strings can do much more than insert values — a `:` inside the braces starts a **format specifier** that controls width, alignment, decimal places, and more.
+
+```python title="fstring_format.py" showLineNumbers{1} {1-6}
+pi = 3.14159
+price = 1234.5
+ratio = 0.875
+
+print(f"{pi:.2f}") # 3.14 -> 2 decimal places
+print(f"{price:,.2f}") # 1,234.50 -> thousands separator
+print(f"{ratio:.1%}") # 87.5% -> percentage
+print(f"{42:05d}") # 00042 -> pad with zeros to width 5
+print(f"{'hi':>10}") # ' hi' -> right-align in 10 cols
+```
+
+You can also print the expression *and* its value with a trailing `=` (great for debugging):
+
+```python title="fstring_debug.py" showLineNumbers{1} {2}
+x = 10
+print(f"{x = }") # x = 10
+```
+
### String Formatting
The `format()` method allows for more controlled string formatting:
@@ -131,6 +154,37 @@ This is a lineThis is another line
In this example, we've used the `end` parameter to remove the newline character from the first `print()` function. This allows us to print the second line on the same line as the first.
+**Diagram**:
+```mermaid title="print() parameters" desc="How sep, end, and file shape the output"
+graph TD
+ A["print(a, b, c, sep, end, file)"] --> B["sep: placed BETWEEN values"]
+ A --> C["end: placed AFTER the last value"]
+ A --> D["file: where output goes (default: screen)"]
+```
+
+### Unpacking a List into print
+
+The `*` operator unpacks an iterable so each element becomes a separate argument — combine it with `sep` to format collections neatly:
+
+```python title="unpack_print.py" showLineNumbers{1} {1-2}
+fruits = ["apple", "banana", "cherry"]
+print(*fruits, sep=", ") # apple, banana, cherry
+```
+
+### Printing to Standard Error
+
+By default `print()` writes to standard output. Pass `file=sys.stderr` to send messages to the error stream instead — useful for warnings and logs that should not mix with normal output. The `flush=True` argument forces the output to appear immediately.
+
+```python title="stderr.py" showLineNumbers{1} {1,3}
+import sys
+print("Normal result")
+print("Something went wrong", file=sys.stderr)
+print("Loading...", end="", flush=True) # appears at once, no buffering
+```
+
+:::note
+`print()` itself returns `None` — it produces output as a side effect, it does not give back a value you can store.
+:::
## Printing Variables and Expressions
diff --git a/src/content/docs/tutorials/Python Comprehensions/Comprehensions.mdx b/src/content/docs/tutorials/Python Comprehensions/Comprehensions.mdx
new file mode 100644
index 00000000..288e3ced
--- /dev/null
+++ b/src/content/docs/tutorials/Python Comprehensions/Comprehensions.mdx
@@ -0,0 +1,294 @@
+---
+title: Comprehensions in Python
+description: Learn about list, dictionary, and set comprehensions in Python. Master the compact syntax for building collections, add conditions and nested loops, and understand when comprehensions improve readability over traditional loops.
+sidebar:
+ order: 100
+---
+
+## Mastering Comprehensions in Python: A Comprehensive Guide
+Comprehensions are one of Python's most loved features. They let you build a list, dictionary, or set from an existing iterable in a single, readable line — replacing a multi-line `for` loop and `append()` call. In this guide, we'll explore list, dictionary, and set comprehensions, add filtering conditions, and cover nested comprehensions.
+
+## What is a Comprehension?
+A **comprehension** is a concise way to create a collection by transforming and filtering items from another iterable. Instead of writing a loop that appends to a list, you describe the result directly.
+
+Compare the traditional loop with a list comprehension:
+
+```python title="loop_vs_comprehension.py" showLineNumbers{1} {1-9}
+# Traditional loop
+squares = []
+for x in range(5):
+ squares.append(x * x)
+print(squares)
+
+# List comprehension - same result, one line
+squares = [x * x for x in range(5)]
+print(squares)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-3}
+C:\Users\Your Name> python loop_vs_comprehension.py
+[0, 1, 4, 9, 16]
+[0, 1, 4, 9, 16]
+```
+
+## List Comprehension
+A list comprehension builds a new list. It is written inside square brackets `[]`.
+
+The syntax of a list comprehension in Python is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1}
+[expression for item in iterable]
+```
+
+- **expression** — what to put in the new list (often a transformation of `item`).
+- **item** — the loop variable.
+- **iterable** — the source you loop over.
+
+```python title="list_comp.py" showLineNumbers{1} {1-5}
+fruits = ["apple", "banana", "cherry"]
+
+# Uppercase every fruit
+upper = [fruit.upper() for fruit in fruits]
+print(upper)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python list_comp.py
+['APPLE', 'BANANA', 'CHERRY']
+```
+
+## Adding a Condition (Filtering)
+You can add an `if` clause at the end to keep only items that match a condition.
+
+The syntax with a condition is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1}
+[expression for item in iterable if condition]
+```
+
+```python title="filter_comp.py" showLineNumbers{1} {1-4}
+numbers = range(10)
+
+# Keep only the even numbers
+evens = [n for n in numbers if n % 2 == 0]
+print(evens)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python filter_comp.py
+[0, 2, 4, 6, 8]
+```
+
+## Conditional Expression (if-else)
+To *transform* items differently based on a condition (rather than filter them out), put an `if-else` *before* the `for`. This is a ternary expression.
+
+```python title="if_else_comp.py" showLineNumbers{1} {1-3}
+numbers = range(5)
+labels = ["even" if n % 2 == 0 else "odd" for n in numbers]
+print(labels)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python if_else_comp.py
+['even', 'odd', 'even', 'odd', 'even']
+```
+
+:::caution
+Placement matters! A **filtering** `if` goes *after* the `for` (`[x for x in it if cond]`). A **transforming** `if-else` goes *before* the `for` (`[a if cond else b for x in it]`).
+:::
+
+## Dictionary Comprehension
+A dictionary comprehension builds a dictionary. It uses curly braces `{}` and a `key: value` pair.
+
+The syntax of a dictionary comprehension is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1}
+{key_expression: value_expression for item in iterable}
+```
+
+```python title="dict_comp.py" showLineNumbers{1} {1-4}
+numbers = [1, 2, 3, 4]
+
+# Map each number to its square
+squares = {n: n * n for n in numbers}
+print(squares)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python dict_comp.py
+{1: 1, 2: 4, 3: 9, 4: 16}
+```
+
+A common use is swapping keys and values, or building a lookup from two lists with `zip()`:
+
+```python title="dict_zip.py" showLineNumbers{1} {1-4}
+names = ["Alice", "Bob"]
+ages = [30, 25]
+people = {name: age for name, age in zip(names, ages)}
+print(people)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python dict_zip.py
+{'Alice': 30, 'Bob': 25}
+```
+
+## Set Comprehension
+A set comprehension builds a set (unique, unordered values). It uses curly braces `{}` like a dict, but with single values instead of pairs.
+
+The syntax of a set comprehension is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1}
+{expression for item in iterable}
+```
+
+```python title="set_comp.py" showLineNumbers{1} {1-4}
+words = ["hi", "hello", "hey", "hi", "hello"]
+
+# Unique word lengths
+lengths = {len(word) for word in words}
+print(lengths)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python set_comp.py
+{2, 5, 3}
+```
+
+## Nested Comprehensions
+You can use more than one `for` clause to loop over nested data, such as flattening a list of lists.
+
+```python title="nested_comp.py" showLineNumbers{1} {1-4}
+matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
+
+# Flatten the matrix into a single list
+flat = [value for row in matrix for value in row]
+print(flat)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python nested_comp.py
+[1, 2, 3, 4, 5, 6, 7, 8, 9]
+```
+
+:::note
+Read nested comprehensions left to right, exactly as you would write the nested loops: the first `for` is the outer loop, the second `for` is the inner loop.
+:::
+
+## When to Use Comprehensions
+| Use a comprehension when... | Avoid a comprehension when... |
+| :--- | :--- |
+| The logic fits on one readable line | The logic needs many conditions or side effects |
+| You are transforming or filtering an iterable | You need `try/except` inside the loop |
+| Building a new list, dict, or set | You only loop for side effects (just use a `for` loop) |
+
+:::tip
+If a comprehension becomes hard to read, switch back to a normal `for` loop. Comprehensions should make code *clearer*, not denser.
+:::
+
+## Conclusion
+Comprehensions provide a compact, expressive way to build lists, dictionaries, and sets directly from iterables. List comprehensions use `[]`, dictionary comprehensions use `{key: value}`, and set comprehensions use `{}` with single values. You can add a trailing `if` to filter, a leading `if-else` to transform conditionally, and stack multiple `for` clauses to handle nested data. Used wisely, comprehensions make your code shorter and more readable. For more hands-on examples and in-depth tutorials, explore our resources on Python Central Hub!
+
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+## Try it: Comprehensions Exercises
+
+### Exercise 1 – Basic List Comprehension
+
+
+
+### Exercise 2 – Comprehension with a Condition
+
+
+
+### Exercise 3 – Dictionary Comprehension
+
+
diff --git a/src/content/docs/tutorials/Python Context Managers/Context Managers.mdx b/src/content/docs/tutorials/Python Context Managers/Context Managers.mdx
new file mode 100644
index 00000000..29b67ed8
--- /dev/null
+++ b/src/content/docs/tutorials/Python Context Managers/Context Managers.mdx
@@ -0,0 +1,296 @@
+---
+title: Context Managers (with statement) in Python
+description: Learn about context managers and the with statement in Python. Understand automatic resource management, the __enter__ and __exit__ methods, building class-based context managers, and the contextlib.contextmanager decorator.
+sidebar:
+ order: 104
+---
+
+## Mastering Context Managers in Python: A Comprehensive Guide
+Opening a file, connecting to a database, or acquiring a lock all share a pattern: you set something up, use it, and must clean it up afterwards — even if an error occurs. **Context managers** automate this setup-and-teardown through the `with` statement, guaranteeing cleanup happens. In this guide, we'll explore the `with` statement, the `__enter__` and `__exit__` methods, and the `contextlib` module.
+
+## The Problem Context Managers Solve
+Without a context manager, you must remember to release resources manually — and handle errors so cleanup still runs.
+
+```python title="manual.py" showLineNumbers{1} {1-7}
+# Manual cleanup - easy to forget, fragile if an error occurs
+file = open("data.txt", "w")
+try:
+ file.write("Hello")
+finally:
+ file.close() # must remember this, even on error
+```
+
+The `with` statement does all of this for you.
+
+## The with Statement
+The `with` statement wraps a block of code so that resources are automatically cleaned up when the block ends — whether it ends normally or because of an exception.
+
+The syntax of the `with` statement in Python is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1-2}
+with expression as variable:
+ # code that uses the resource
+```
+
+The classic example is file handling:
+
+```python title="with_file.py" showLineNumbers{1} {1-4}
+with open("data.txt", "w") as file:
+ file.write("Hello, World!")
+# file is automatically closed here - even if write() raised an error
+print("File closed:", file.closed)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python with_file.py
+File closed: True
+```
+
+**Diagram**:
+```mermaid title="with statement flow" desc="How the with statement manages a resource"
+graph TD
+ A[Enter with block] --> B[Call __enter__: set up resource]
+ B --> C[Run the indented body]
+ C --> D{Exception?}
+ D -->|No| E[Call __exit__: clean up]
+ D -->|Yes| F[Call __exit__: clean up, then handle]
+ E --> G[Continue program]
+ F --> G[Continue program]
+```
+
+:::tip
+Always use `with open(...)` instead of a bare `open(...)`. It guarantees the file is closed, freeing the operating system handle even if your code crashes.
+:::
+
+## What is a Context Manager?
+A **context manager** is any object that implements two special methods:
+
+- `__enter__(self)` — runs when the `with` block is entered. Its return value is bound to the `as` variable.
+- `__exit__(self, exc_type, exc_value, traceback)` — runs when the block exits, for cleanup. It receives details of any exception that occurred.
+
+## Building a Class-Based Context Manager
+Let's build a `Timer` that measures how long the `with` block takes.
+
+```python title="timer.py" showLineNumbers{1} {1-18}
+import time
+
+class Timer:
+ def __enter__(self):
+ self.start = time.time()
+ print("Timer started")
+ return self # bound to the 'as' variable
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ elapsed = time.time() - self.start
+ print(f"Elapsed: {elapsed:.2f}s")
+ return False # do not suppress exceptions
+
+with Timer() as t:
+ total = sum(range(1_000_000))
+
+print("Done")
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-4}
+C:\Users\Your Name> python timer.py
+Timer started
+Elapsed: 0.03s
+Done
+```
+
+### The __exit__ Return Value
+If `__exit__` returns `True`, any exception raised inside the block is **suppressed** (swallowed). If it returns `False` (or `None`), the exception **propagates** normally after cleanup. Most context managers return `False`.
+
+```python title="suppress.py" showLineNumbers{1} {1-13}
+class Suppressor:
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if exc_type is ValueError:
+ print("Suppressed a ValueError")
+ return True # swallow ValueError
+ return False
+
+with Suppressor():
+ raise ValueError("boom") # caught and suppressed
+print("Program continues")
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-3}
+C:\Users\Your Name> python suppress.py
+Suppressed a ValueError
+Program continues
+```
+
+## The contextlib Approach
+Writing a whole class can be heavy. The `contextlib.contextmanager` decorator lets you build a context manager from a simple generator function. The code **before** `yield` is the setup (`__enter__`), and the code **after** `yield` is the cleanup (`__exit__`).
+
+```python title="contextlib_timer.py" showLineNumbers{1} {1-15}
+from contextlib import contextmanager
+import time
+
+@contextmanager
+def timer():
+ start = time.time() # setup (like __enter__)
+ print("Timer started")
+ yield # the 'with' body runs here
+ elapsed = time.time() - start
+ print(f"Elapsed: {elapsed:.2f}s") # cleanup (like __exit__)
+
+with timer():
+ total = sum(range(1_000_000))
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-3}
+C:\Users\Your Name> python contextlib_timer.py
+Timer started
+Elapsed: 0.03s
+```
+
+:::caution
+With `@contextmanager`, wrap the `yield` in a `try/finally` if cleanup must run even when the body raises an exception. The `finally` block guarantees the teardown code runs.
+:::
+
+## Multiple Context Managers
+You can manage several resources in one `with` statement by separating them with commas.
+
+```python title="multiple.py" showLineNumbers{1} {1-3}
+with open("input.txt") as src, open("output.txt", "w") as dst:
+ dst.write(src.read())
+# both files are closed automatically
+```
+
+## Conclusion
+Context managers automate resource setup and cleanup through the `with` statement, guaranteeing teardown even when errors occur. Any object with `__enter__` and `__exit__` methods is a context manager; `__enter__` sets up and returns the resource, while `__exit__` cleans up and decides whether to suppress exceptions. For lighter cases, the `@contextmanager` decorator from `contextlib` turns a generator into a context manager using `yield` to split setup from cleanup. Reach for `with` whenever you handle files, locks, or connections. For more hands-on examples and in-depth tutorials, explore our resources on Python Central Hub!
+
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+## Try it: Context Managers Exercises
+
+### Exercise 1 – The with Statement
+
+
+
+### Exercise 2 – Class-Based Context Manager
+
+
+
+### Exercise 3 – contextlib Generator
+
+
diff --git a/src/content/docs/tutorials/Python Exception Handling/Exception Handling.mdx b/src/content/docs/tutorials/Python Exception Handling/Exception Handling.mdx
new file mode 100644
index 00000000..4581045b
--- /dev/null
+++ b/src/content/docs/tutorials/Python Exception Handling/Exception Handling.mdx
@@ -0,0 +1,385 @@
+---
+title: Exception Handling in Python
+description: Learn about exception handling in Python. Master the try, except, else, and finally blocks, learn how to raise exceptions with the raise keyword, and create your own custom exception classes to write robust, crash-resistant programs.
+sidebar:
+ order: 98
+---
+
+## Mastering Exception Handling in Python: A Comprehensive Guide
+Errors are unavoidable in real programs — a file may be missing, a user may type text where a number was expected, or a network call may fail. Exception handling is the mechanism Python provides to detect these errors and respond to them gracefully instead of crashing. In this guide, we'll explore the `try`, `except`, `else`, and `finally` blocks, the `raise` keyword, and how to build your own custom exceptions.
+
+## What is an Exception?
+An **exception** is an event that occurs during the execution of a program that disrupts the normal flow of instructions. When Python encounters an error it cannot handle, it *raises* an exception. If the exception is not handled, the program stops and prints a **traceback**.
+
+```python title="error.py" showLineNumbers{1} {2}
+# An unhandled exception
+print(10 / 0)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-4}
+C:\Users\Your Name> python error.py
+Traceback (most recent call last):
+ File "error.py", line 2, in
+ print(10 / 0)
+ZeroDivisionError: division by zero
+```
+
+The program never reaches any code after line 2 — the `ZeroDivisionError` terminates it. Exception handling lets us catch that error and continue.
+
+## Errors vs. Exceptions
+- **Syntax errors** are mistakes in the structure of the code (a missing colon, an unclosed bracket). They are caught *before* the program runs and cannot be handled at runtime.
+- **Exceptions** are raised *while* the program runs. These are the errors we handle with `try`/`except`.
+
+```python title="syntax_error.py" showLineNumbers{1} {2}
+# Syntax error - cannot be caught with try/except
+if True
+ print("missing colon")
+```
+
+## The try-except Block
+The `try` block contains code that *might* raise an exception. The `except` block contains the code that runs *if* an exception occurs.
+
+The syntax of the `try-except` block in Python is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1-4}
+try:
+ # code that might raise an exception
+except ExceptionType:
+ # code that runs if the exception occurs
+```
+
+**Diagram**:
+```mermaid title="try-except flow" desc="Flow of the try-except block in Python"
+graph TD
+ A[Start] --> B[Run try block]
+ B --> C{Exception raised?}
+ C -->|No| D[Skip except block]
+ C -->|Yes| E[Run except block]
+ D --> F[End]
+ E --> F[End]
+```
+
+The following example demonstrates how to catch a `ZeroDivisionError`:
+
+```python title="try_except.py" showLineNumbers{1} {1-4}
+try:
+ result = 10 / 0
+ print(result)
+except ZeroDivisionError:
+ print("You cannot divide by zero!")
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python try_except.py
+You cannot divide by zero!
+```
+
+In the above example, the division raises a `ZeroDivisionError`. Python jumps to the matching `except` block, prints the message, and the program continues normally instead of crashing.
+
+:::tip
+Catch the most specific exception you can. Catching `ZeroDivisionError` is better than catching every error, because it documents exactly what you expect to go wrong.
+:::
+
+## Catching the Exception Object
+You can capture the exception object itself using the `as` keyword. This gives you access to the error message.
+
+```python title="as_keyword.py" showLineNumbers{1} {3-4}
+try:
+ number = int("hello")
+except ValueError as error:
+ print("Conversion failed:", error)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python as_keyword.py
+Conversion failed: invalid literal for int() with base 10: 'hello'
+```
+
+## Handling Multiple Exceptions
+A single `try` block can be followed by several `except` blocks. Python runs the first one that matches the raised exception.
+
+```python title="multiple.py" showLineNumbers{1} {1-8}
+try:
+ value = int(input("Enter a number: "))
+ result = 100 / value
+ print(result)
+except ValueError:
+ print("That was not a valid number.")
+except ZeroDivisionError:
+ print("You cannot divide by zero.")
+```
+
+You can also handle several exception types in one block by passing a tuple:
+
+```python title="tuple_except.py" showLineNumbers{1} {3}
+try:
+ risky_operation()
+except (ValueError, TypeError, KeyError) as error:
+ print("Something went wrong:", error)
+```
+
+:::caution
+A bare `except:` (with no exception type) catches *everything*, including `KeyboardInterrupt` and `SystemExit`. Avoid it. If you must catch broadly, use `except Exception:` instead.
+:::
+
+## The else Clause
+The `else` block runs **only if no exception was raised** in the `try` block. It is useful for code that should run after a successful `try`, keeping the `try` block focused on the risky operation alone.
+
+```python title="else_clause.py" showLineNumbers{1} {1-7}
+try:
+ number = int("42")
+except ValueError:
+ print("Invalid number.")
+else:
+ print("Conversion succeeded:", number)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python else_clause.py
+Conversion succeeded: 42
+```
+
+## The finally Clause
+The `finally` block runs **no matter what** — whether or not an exception was raised, and even if a `return` statement executes. It is used for cleanup tasks such as closing files or releasing resources.
+
+```python title="finally_clause.py" showLineNumbers{1} {1-8}
+try:
+ file = open("data.txt", "r")
+ data = file.read()
+except FileNotFoundError:
+ print("File not found.")
+finally:
+ print("Cleaning up...")
+```
+
+**Diagram**:
+```mermaid title="try-except-else-finally" desc="Full exception handling flow in Python"
+graph TD
+ A[Start] --> B[Run try block]
+ B --> C{Exception?}
+ C -->|No| D[Run else block]
+ C -->|Yes| E[Run except block]
+ D --> F[Run finally block]
+ E --> F[Run finally block]
+ F --> G[End]
+```
+
+:::note
+The order is fixed: `try` → `except` → `else` → `finally`. The `else` and `finally` clauses are both optional.
+:::
+
+## The raise Keyword
+You can deliberately raise an exception using the `raise` keyword. This is useful for signalling that something is wrong with the input to your function.
+
+```python title="raise.py" showLineNumbers{1} {1-6}
+def set_age(age):
+ if age < 0:
+ raise ValueError("Age cannot be negative")
+ print("Age set to", age)
+
+set_age(-5)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-4}
+C:\Users\Your Name> python raise.py
+Traceback (most recent call last):
+ ...
+ValueError: Age cannot be negative
+```
+
+### Re-raising an Exception
+Inside an `except` block, a bare `raise` re-raises the current exception after you have done some work (such as logging).
+
+```python title="reraise.py" showLineNumbers{1} {3-5}
+try:
+ risky()
+except ValueError:
+ print("Logging the error before re-raising...")
+ raise
+```
+
+## Custom Exceptions
+You can define your own exception types by subclassing the built-in `Exception` class. Custom exceptions make your code more readable and let callers catch *your* specific error.
+
+```python title="custom_exception.py" showLineNumbers{1} {1-13}
+class InsufficientFundsError(Exception):
+ """Raised when a withdrawal exceeds the available balance."""
+ pass
+
+def withdraw(balance, amount):
+ if amount > balance:
+ raise InsufficientFundsError(
+ f"Cannot withdraw {amount}; balance is only {balance}"
+ )
+ return balance - amount
+
+try:
+ withdraw(100, 250)
+except InsufficientFundsError as error:
+ print("Transaction failed:", error)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python custom_exception.py
+Transaction failed: Cannot withdraw 250; balance is only 100
+```
+
+:::tip
+By convention, custom exception class names end with `Error`. Always inherit from `Exception` (not from `BaseException`).
+:::
+
+## Common Built-in Exceptions
+| Exception | Raised when |
+| :--- | :--- |
+| `ValueError` | A function gets an argument of the right type but wrong value (e.g. `int("abc")`). |
+| `TypeError` | An operation is applied to an object of the wrong type. |
+| `KeyError` | A dictionary key is not found. |
+| `IndexError` | A sequence index is out of range. |
+| `FileNotFoundError` | A file or directory is requested but does not exist. |
+| `ZeroDivisionError` | The second argument of a division or modulo is zero. |
+| `AttributeError` | An attribute reference or assignment fails. |
+| `ImportError` | An `import` statement fails to find the module. |
+
+## Conclusion
+Exception handling lets your programs respond to errors gracefully instead of crashing. The `try` block holds risky code, `except` handles failures, `else` runs on success, and `finally` always runs for cleanup. The `raise` keyword lets you signal errors, and custom exception classes make your error handling expressive and precise. Master these tools and your Python programs will be far more robust. For more hands-on examples and in-depth tutorials, explore our resources on Python Central Hub!
+
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+## Try it: Exception Handling Exercises
+
+### Exercise 1 – Catch a ZeroDivisionError
+
+
+
+### Exercise 2 – try / except / else / finally
+
+
+
+### Exercise 3 – Raise a Custom Exception
+
+
diff --git a/src/content/docs/tutorials/Python Function/Args and Kwargs.mdx b/src/content/docs/tutorials/Python Function/Args and Kwargs.mdx
new file mode 100644
index 00000000..097fd68e
--- /dev/null
+++ b/src/content/docs/tutorials/Python Function/Args and Kwargs.mdx
@@ -0,0 +1,285 @@
+---
+title: "*args and **kwargs in Python"
+description: Learn about arbitrary arguments in Python with *args and **kwargs. Understand how to accept any number of positional and keyword arguments, the packing and unpacking operators, argument order, and how to forward arguments between functions.
+sidebar:
+ order: 43.5
+---
+
+## Mastering *args and **kwargs in Python: A Comprehensive Guide
+Sometimes you do not know in advance how many arguments a function will receive — think of `print()`, which accepts any number of values. Python solves this with `*args` and `**kwargs`, which let a function accept an arbitrary number of positional and keyword arguments. In this guide, we'll explore packing and unpacking, the correct argument order, and how to pass arguments between functions.
+
+## Recap: Positional and Keyword Arguments
+Before arbitrary arguments, recall the two basic kinds:
+
+- **Positional arguments** are matched by their position: `add(2, 3)`.
+- **Keyword arguments** are matched by name: `add(x=2, y=3)`.
+
+`*args` and `**kwargs` extend these so a function can take *as many as the caller passes*.
+
+## *args – Arbitrary Positional Arguments
+Putting a single `*` before a parameter name tells Python to **pack** all extra positional arguments into a **tuple**. By convention the parameter is named `args`, but any name works.
+
+The syntax of `*args` in Python is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1-2}
+def function_name(*args):
+ # args is a tuple of all positional arguments
+```
+
+```python title="args_basic.py" showLineNumbers{1} {1-6}
+def add_all(*args):
+ print("args is:", args) # a tuple
+ return sum(args)
+
+print(add_all(1, 2, 3))
+print(add_all(10, 20, 30, 40))
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-5}
+C:\Users\Your Name> python args_basic.py
+args is: (1, 2, 3)
+6
+args is: (10, 20, 30, 40)
+100
+```
+
+:::note
+`*args` collects only the *extra* positional arguments. You can have regular parameters before it: `def greet(greeting, *names)` — `greeting` takes the first argument and `*names` packs the rest.
+:::
+
+## **kwargs – Arbitrary Keyword Arguments
+Putting a double `**` before a parameter name **packs** all extra keyword arguments into a **dictionary**. By convention it is named `kwargs`.
+
+The syntax of `**kwargs` in Python is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1-2}
+def function_name(**kwargs):
+ # kwargs is a dict of all keyword arguments
+```
+
+```python title="kwargs_basic.py" showLineNumbers{1} {1-6}
+def build_profile(**kwargs):
+ print("kwargs is:", kwargs) # a dict
+ for key, value in kwargs.items():
+ print(f"{key}: {value}")
+
+build_profile(name="Alice", age=30, city="Paris")
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-5}
+C:\Users\Your Name> python kwargs_basic.py
+kwargs is: {'name': 'Alice', 'age': 30, 'city': 'Paris'}
+name: Alice
+age: 30
+city: Paris
+```
+
+## Combining Everything
+You can mix regular parameters, `*args`, default parameters, and `**kwargs` in one function. The order is **fixed**:
+
+```python title="Syntax" showLineNumbers{1} {1}
+def function(positional, *args, keyword_default=value, **kwargs):
+```
+
+The required order is:
+1. Standard positional parameters
+2. `*args`
+3. Keyword-only / default parameters
+4. `**kwargs`
+
+```python title="combined.py" showLineNumbers{1} {1-9}
+def order(main, *extras, sauce="ketchup", **details):
+ print("Main:", main)
+ print("Extras:", extras)
+ print("Sauce:", sauce)
+ print("Details:", details)
+
+order("Burger", "Fries", "Coke", sauce="mayo", size="large", spicy=True)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-5}
+C:\Users\Your Name> python combined.py
+Main: Burger
+Extras: ('Fries', 'Coke')
+Sauce: mayo
+Details: {'size': 'large', 'spicy': True}
+```
+
+**Diagram**:
+```mermaid title="argument packing" desc="How arguments are distributed into parameters"
+graph TD
+ A["order('Burger', 'Fries', 'Coke', sauce='mayo', size='large', spicy=True)"] --> B[main = 'Burger']
+ A --> C["extras = ('Fries', 'Coke')"]
+ A --> D[sauce = 'mayo']
+ A --> E["details = {size, spicy}"]
+```
+
+## Unpacking with * and **
+The `*` and `**` operators also work in the *opposite* direction. When **calling** a function, `*` unpacks a list/tuple into positional arguments and `**` unpacks a dict into keyword arguments.
+
+```python title="unpacking.py" showLineNumbers{1} {1-11}
+def introduce(name, age, city):
+ print(f"{name} is {age} and lives in {city}")
+
+# Unpack a list into positional arguments
+data = ["Bob", 25, "London"]
+introduce(*data)
+
+# Unpack a dict into keyword arguments
+info = {"name": "Carol", "age": 28, "city": "Berlin"}
+introduce(**info)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-3}
+C:\Users\Your Name> python unpacking.py
+Bob is 25 and lives in London
+Carol is 28 and lives in Berlin
+```
+
+:::tip
+Packing happens in the function *definition* (`def f(*args)`); unpacking happens at the *call site* (`f(*mylist)`). Same symbols, opposite directions.
+:::
+
+## Forwarding Arguments
+A common pattern is a wrapper function that accepts anything and passes it straight through to another function. This is how decorators wrap arbitrary functions.
+
+```python title="forwarding.py" showLineNumbers{1} {1-10}
+def logged(func, *args, **kwargs):
+ print(f"Calling {func.__name__} with {args} {kwargs}")
+ return func(*args, **kwargs) # forward everything
+
+def power(base, exponent):
+ return base ** exponent
+
+print(logged(power, 2, exponent=10))
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-3}
+C:\Users\Your Name> python forwarding.py
+Calling power with (2,) {'exponent': 10}
+1024
+```
+
+## Conclusion
+`*args` and `**kwargs` let functions accept any number of arguments: `*args` packs extra positional arguments into a tuple, and `**kwargs` packs extra keyword arguments into a dictionary. The same `*` and `**` operators unpack sequences and dictionaries back into arguments at the call site. Combine them with regular parameters in the fixed order — positional, `*args`, keyword defaults, `**kwargs` — and use them to write flexible wrappers and decorators. For more hands-on examples and in-depth tutorials, explore our resources on Python Central Hub!
+
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+## Try it: *args and **kwargs Exercises
+
+### Exercise 1 – Collect with *args
+
+
+
+### Exercise 2 – Collect with **kwargs
+
+
+
+### Exercise 3 – Unpacking at the Call Site
+
+
diff --git a/src/content/docs/tutorials/Python Function/Function Annotations.mdx b/src/content/docs/tutorials/Python Function/Function Annotations.mdx
index 2bbef4a0..3e071985 100644
--- a/src/content/docs/tutorials/Python Function/Function Annotations.mdx
+++ b/src/content/docs/tutorials/Python Function/Function Annotations.mdx
@@ -5,6 +5,10 @@ sidebar:
order: 44
---
+:::tip[Go deeper]
+This page covers the basics of annotations. For `Optional`, `Union`, the `|` syntax, `Callable`, generics (`TypeVar`), type aliases, and static checking with mypy, see the dedicated [Type Hints & the typing Module](../../modern-python/type-hints-and-typing/) tutorial.
+:::
+
## Unraveling Function Annotations in Python: A Comprehensive Guide
Function annotations in Python provide a way to attach metadata, including type hints, to the parameters and return values of functions. Introduced in PEP 3107 and enhanced in subsequent PEPs (Python Enhancement Proposals), function annotations offer a mechanism for developers to provide additional information about the expected types and purpose of function parameters and return values. In this comprehensive guide, we'll explore the syntax, use cases, and best practices associated with function annotations in Python.
diff --git a/src/content/docs/tutorials/Python Functional Programming/Closures and Scope.mdx b/src/content/docs/tutorials/Python Functional Programming/Closures and Scope.mdx
new file mode 100644
index 00000000..820c1b8c
--- /dev/null
+++ b/src/content/docs/tutorials/Python Functional Programming/Closures and Scope.mdx
@@ -0,0 +1,326 @@
+---
+title: Closures and Scope (LEGB) in Python
+description: Learn about variable scope and closures in Python. Master the LEGB rule (Local, Enclosing, Global, Built-in), the global and nonlocal keywords, and how closures capture variables from their enclosing function.
+sidebar:
+ order: 101
+---
+
+## Mastering Closures and Scope in Python: A Comprehensive Guide
+Every variable in Python lives in a **scope** — a region of the program where that name is visible. Understanding scope explains why some variables are accessible inside a function and others are not, and why changing a variable inside a function sometimes affects the outside and sometimes does not. **Closures** build on scope: they let an inner function remember variables from the function that created it. In this guide, we'll explore the LEGB rule, the `global` and `nonlocal` keywords, and closures.
+
+## What is Scope?
+**Scope** determines where a name (variable, function) can be accessed. Python resolves names using the **LEGB rule**, searching four scopes in order:
+
+1. **L**ocal — names assigned inside the current function.
+2. **E**nclosing — names in any enclosing (outer) function.
+3. **G**lobal — names at the top level of the module.
+4. **B**uilt-in — names pre-defined by Python (`len`, `print`, `range`, ...).
+
+**Diagram**:
+```mermaid title="LEGB rule" desc="Order Python searches scopes to resolve a name"
+graph TD
+ A[Name used] --> B[Local scope?]
+ B -->|Found| Z[Use it]
+ B -->|Not found| C[Enclosing scope?]
+ C -->|Found| Z
+ C -->|Not found| D[Global scope?]
+ D -->|Found| Z
+ D -->|Not found| E[Built-in scope?]
+ E -->|Found| Z
+ E -->|Not found| F[NameError]
+```
+
+The following example shows all four levels being resolved:
+
+```python title="legb.py" showLineNumbers{1} {1-12}
+x = "global" # Global scope
+
+def outer():
+ y = "enclosing" # Enclosing scope
+
+ def inner():
+ z = "local" # Local scope
+ print(z) # found in Local
+ print(y) # found in Enclosing
+ print(x) # found in Global
+ print(len) # found in Built-in
+
+ inner()
+
+outer()
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-5}
+C:\Users\Your Name> python legb.py
+local
+enclosing
+global
+
+```
+
+## Local Scope
+A variable assigned inside a function is **local** to that function and disappears when the function returns. It cannot be accessed from outside.
+
+```python title="local.py" showLineNumbers{1} {1-5}
+def greet():
+ message = "Hello" # local variable
+ print(message)
+
+greet()
+print(message) # NameError: 'message' is not defined
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-4}
+C:\Users\Your Name> python local.py
+Hello
+Traceback (most recent call last):
+NameError: name 'message' is not defined
+```
+
+## Global Scope and the global Keyword
+By default, a function can **read** a global variable but **assigning** to a name inside a function creates a *new local* variable. To modify a global variable from inside a function, use the `global` keyword.
+
+```python title="global_keyword.py" showLineNumbers{1} {1-12}
+count = 0
+
+def increment_wrong():
+ count = count + 1 # ERROR: count is treated as local
+
+def increment_right():
+ global count # tell Python to use the global 'count'
+ count = count + 1
+
+increment_right()
+increment_right()
+print(count)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python global_keyword.py
+2
+```
+
+:::caution
+Overusing `global` makes code hard to follow and test. Prefer passing values in as arguments and returning results instead of mutating globals.
+:::
+
+## Enclosing Scope and the nonlocal Keyword
+When you have a function defined inside another function, the inner function can read the outer function's variables. To **reassign** an enclosing variable, use the `nonlocal` keyword.
+
+```python title="nonlocal_keyword.py" showLineNumbers{1} {1-13}
+def counter():
+ count = 0
+
+ def increment():
+ nonlocal count # refer to 'count' in the enclosing scope
+ count += 1
+ return count
+
+ print(increment())
+ print(increment())
+ print(increment())
+
+counter()
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-4}
+C:\Users\Your Name> python nonlocal_keyword.py
+1
+2
+3
+```
+
+:::note
+`global` reaches all the way to module scope. `nonlocal` reaches only to the nearest enclosing *function* scope — not to the global scope.
+:::
+
+## What is a Closure?
+A **closure** is a function that remembers the variables from its enclosing scope even after that outer function has finished running. Three conditions make a closure:
+
+1. There is a nested (inner) function.
+2. The inner function refers to a variable from the enclosing function.
+3. The enclosing function returns the inner function.
+
+```python title="closure.py" showLineNumbers{1} {1-12}
+def make_multiplier(factor):
+ # 'factor' is captured by the inner function
+ def multiplier(number):
+ return number * factor
+ return multiplier # return the function, do not call it
+
+double = make_multiplier(2)
+triple = make_multiplier(3)
+
+print(double(5)) # 10
+print(triple(5)) # 15
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-3}
+C:\Users\Your Name> python closure.py
+10
+15
+```
+
+Even though `make_multiplier` has already returned, `double` still remembers `factor = 2` and `triple` still remembers `factor = 3`. Each closure carries its own captured value.
+
+**Diagram**:
+```mermaid title="closure" desc="How a closure captures an enclosing variable"
+graph TD
+ A[Call make_multiplier 2] --> B[factor = 2 captured]
+ B --> C[Return inner function 'double']
+ C --> D[make_multiplier finishes]
+ D --> E[double still remembers factor = 2]
+ E --> F[double 5 returns 10]
+```
+
+## Inspecting a Closure
+You can see the values a closure has captured through its `__closure__` attribute.
+
+```python title="inspect_closure.py" showLineNumbers{1} {1-5}
+double = make_multiplier(2)
+print(double.__closure__[0].cell_contents)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python inspect_closure.py
+2
+```
+
+## Why Closures Matter
+- They let you build **function factories** (like `make_multiplier`) that generate customised functions.
+- They are the foundation of **decorators**.
+- They keep data private inside a function without needing a class.
+
+## Conclusion
+Scope controls where names are visible, and Python resolves them with the LEGB rule: Local, then Enclosing, then Global, then Built-in. Use `global` to reassign a module-level variable and `nonlocal` to reassign an enclosing function's variable. Closures take this further — an inner function returned from an outer one remembers the variables it captured, enabling powerful patterns like function factories and decorators. For more hands-on examples and in-depth tutorials, explore our resources on Python Central Hub!
+
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+## Try it: Closures and Scope Exercises
+
+### Exercise 1 – The global Keyword
+
+
+
+### Exercise 2 – The nonlocal Keyword
+
+
+
+### Exercise 3 – Build a Closure
+
+
diff --git a/src/content/docs/tutorials/Python Functional Programming/Map Filter Reduce.mdx b/src/content/docs/tutorials/Python Functional Programming/Map Filter Reduce.mdx
new file mode 100644
index 00000000..fa8d6d53
--- /dev/null
+++ b/src/content/docs/tutorials/Python Functional Programming/Map Filter Reduce.mdx
@@ -0,0 +1,270 @@
+---
+title: Map, Filter, and Reduce in Python
+description: Learn functional programming tools in Python. Master the map() function to transform iterables, filter() to select items, and reduce() from functools to combine values into a single result, and compare them with comprehensions.
+sidebar:
+ order: 102
+---
+
+## Mastering Map, Filter, and Reduce in Python: A Comprehensive Guide
+`map()`, `filter()`, and `reduce()` are the classic tools of **functional programming**. Each takes a function and applies it across an iterable: `map` transforms every item, `filter` selects items that pass a test, and `reduce` combines all items into a single value. They pair naturally with `lambda` functions. In this guide, we'll explore each one with examples and compare them to comprehensions.
+
+## What is Functional Programming?
+Functional programming treats functions as values you can pass around. Instead of writing explicit loops, you describe *what* transformation to apply and let the tool handle the iteration. `map`, `filter`, and `reduce` are the three building blocks.
+
+| Tool | Purpose | Result |
+| :--- | :--- | :--- |
+| `map(func, iterable)` | Apply `func` to every item | An iterator of transformed items |
+| `filter(func, iterable)` | Keep items where `func` returns `True` | An iterator of kept items |
+| `reduce(func, iterable)` | Combine items pairwise into one value | A single value |
+
+## The map() Function
+`map()` applies a function to every element of an iterable and returns a **map object** (an iterator). Convert it to a list to see the results.
+
+The syntax of the `map()` function in Python is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1}
+map(function, iterable)
+```
+
+```python title="map_basic.py" showLineNumbers{1} {1-7}
+numbers = [1, 2, 3, 4]
+
+def square(n):
+ return n * n
+
+squared = map(square, numbers)
+print(list(squared))
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python map_basic.py
+[1, 4, 9, 16]
+```
+
+Most often `map` is used with a `lambda`:
+
+```python title="map_lambda.py" showLineNumbers{1} {1-3}
+numbers = [1, 2, 3, 4]
+squared = map(lambda n: n * n, numbers)
+print(list(squared))
+```
+
+You can also pass **multiple iterables** — the function then receives one item from each:
+
+```python title="map_multi.py" showLineNumbers{1} {1-4}
+a = [1, 2, 3]
+b = [10, 20, 30]
+sums = map(lambda x, y: x + y, a, b)
+print(list(sums))
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python map_multi.py
+[11, 22, 33]
+```
+
+## The filter() Function
+`filter()` keeps only the elements for which the function returns `True`. It also returns an iterator.
+
+The syntax of the `filter()` function in Python is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1}
+filter(function, iterable)
+```
+
+```python title="filter_basic.py" showLineNumbers{1} {1-4}
+numbers = [1, 2, 3, 4, 5, 6]
+
+evens = filter(lambda n: n % 2 == 0, numbers)
+print(list(evens))
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python filter_basic.py
+[2, 4, 6]
+```
+
+:::tip
+If you pass `None` as the function, `filter` keeps only the *truthy* items: `list(filter(None, [0, 1, "", "hi", None]))` gives `[1, 'hi']`.
+:::
+
+## The reduce() Function
+`reduce()` repeatedly applies a function of two arguments to the items of an iterable, accumulating them into a single value. Unlike `map` and `filter`, it lives in the `functools` module and must be imported.
+
+The syntax of the `reduce()` function in Python is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1-2}
+from functools import reduce
+reduce(function, iterable, initializer)
+```
+
+```python title="reduce_sum.py" showLineNumbers{1} {1-5}
+from functools import reduce
+
+numbers = [1, 2, 3, 4]
+total = reduce(lambda acc, n: acc + n, numbers)
+print(total)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python reduce_sum.py
+10
+```
+
+**Diagram**:
+```mermaid title="reduce" desc="How reduce accumulates a list into one value"
+graph LR
+ A["1 + 2 = 3"] --> B["3 + 3 = 6"]
+ B --> C["6 + 4 = 10"]
+ C --> D["Result: 10"]
+```
+
+`reduce` accepts an optional **initializer** — the starting value for the accumulator. It is safer because it defines the result for an empty iterable.
+
+```python title="reduce_init.py" showLineNumbers{1} {1-4}
+from functools import reduce
+
+numbers = [1, 2, 3, 4]
+product = reduce(lambda acc, n: acc * n, numbers, 1) # start at 1
+print(product)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python reduce_init.py
+24
+```
+
+## map / filter vs. Comprehensions
+A list comprehension can do everything `map` and `filter` do, and is often considered more Pythonic and readable.
+
+```python title="vs_comprehension.py" showLineNumbers{1} {1-9}
+numbers = [1, 2, 3, 4, 5, 6]
+
+# map + filter
+result_fp = list(map(lambda n: n * n, filter(lambda n: n % 2 == 0, numbers)))
+
+# equivalent comprehension
+result_comp = [n * n for n in numbers if n % 2 == 0]
+
+print(result_fp, result_comp)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python vs_comprehension.py
+[4, 16, 36] [4, 16, 36]
+```
+
+:::note
+Prefer comprehensions for `map`/`filter`-style work — they read better. Reach for `reduce` (or a plain loop) when you need to fold a sequence into a single accumulated value.
+:::
+
+## Conclusion
+`map`, `filter`, and `reduce` apply a function across an iterable in three different ways: `map` transforms every item, `filter` selects the items that pass a test, and `reduce` collapses the whole iterable into one value. `map` and `filter` are built in and return lazy iterators; `reduce` lives in `functools`. While comprehensions often replace `map` and `filter` more readably, `reduce` remains the go-to for accumulation. For more hands-on examples and in-depth tutorials, explore our resources on Python Central Hub!
+
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+## Try it: Map, Filter, and Reduce Exercises
+
+### Exercise 1 – map()
+
+
+
+### Exercise 2 – filter()
+
+ 10
+big = filter(___, numbers) # replace ___ with the lambda
+
+print(list(big))
+
+# -- Expected Output --------------------------------------------
+# [12, 20, 18]
+# ---------------------------------------------------------------`}
+ solution={`numbers = [5, 12, 7, 20, 3, 18]
+big = filter(lambda n: n > 10, numbers)
+print(list(big))`}
+ sct={`test_output_contains("[12, 20, 18]")
+success_msg("You used filter()!")`}
+ height={200}
+/>
+
+### Exercise 3 – reduce()
+
+
diff --git a/src/content/docs/tutorials/Python Functional Programming/Recursion.mdx b/src/content/docs/tutorials/Python Functional Programming/Recursion.mdx
new file mode 100644
index 00000000..ef225ffb
--- /dev/null
+++ b/src/content/docs/tutorials/Python Functional Programming/Recursion.mdx
@@ -0,0 +1,261 @@
+---
+title: Recursion in Python
+description: Learn about recursion in Python. Understand base cases and recursive cases, write recursive functions for factorials, Fibonacci numbers, and traversals, learn about the recursion limit, and compare recursion with iteration.
+sidebar:
+ order: 103
+---
+
+## Mastering Recursion in Python: A Comprehensive Guide
+**Recursion** is when a function calls itself to solve a smaller version of the same problem. It is a powerful technique for problems that are naturally self-similar — factorials, tree traversals, and divide-and-conquer algorithms. In this guide, we'll explore base cases and recursive cases, build classic recursive functions, look at Python's recursion limit, and compare recursion with iteration.
+
+## What is Recursion?
+A **recursive function** is a function that calls itself. Every recursive solution has two parts:
+
+1. **Base case** — the simplest input, which the function answers directly *without* recursing. This stops the recursion.
+2. **Recursive case** — the function calls itself with a smaller or simpler input, moving toward the base case.
+
+:::caution
+Every recursive function **must** have a base case. Without one, the function calls itself forever and Python raises `RecursionError: maximum recursion depth exceeded`.
+:::
+
+## A First Example: Countdown
+```python title="countdown.py" showLineNumbers{1} {1-8}
+def countdown(n):
+ if n == 0: # base case
+ print("Liftoff!")
+ return
+ print(n)
+ countdown(n - 1) # recursive case - smaller input
+
+countdown(3)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-5}
+C:\Users\Your Name> python countdown.py
+3
+2
+1
+Liftoff!
+```
+
+**Diagram**:
+```mermaid title="recursion call stack" desc="How recursive countdown calls stack and unwind"
+graph TD
+ A["countdown(3)"] --> B["countdown(2)"]
+ B --> C["countdown(1)"]
+ C --> D["countdown(0) base case"]
+ D --> E["Liftoff! returns"]
+```
+
+## The Factorial Function
+The factorial of `n` (written `n!`) is the product of all positive integers up to `n`. It has a natural recursive definition: `n! = n * (n-1)!`, with `0! = 1` as the base case.
+
+```python title="factorial.py" showLineNumbers{1} {1-7}
+def factorial(n):
+ if n == 0 or n == 1: # base case
+ return 1
+ return n * factorial(n - 1) # recursive case
+
+print(factorial(5))
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python factorial.py
+120
+```
+
+Here is how the calls expand and then collapse:
+
+```text title="factorial(5) expansion" showLineNumbers{1}
+factorial(5)
+= 5 * factorial(4)
+= 5 * (4 * factorial(3))
+= 5 * (4 * (3 * factorial(2)))
+= 5 * (4 * (3 * (2 * factorial(1))))
+= 5 * 4 * 3 * 2 * 1
+= 120
+```
+
+## The Fibonacci Sequence
+Each Fibonacci number is the sum of the two before it: `fib(n) = fib(n-1) + fib(n-2)`, with `fib(0) = 0` and `fib(1) = 1` as base cases.
+
+```python title="fibonacci.py" showLineNumbers{1} {1-8}
+def fib(n):
+ if n < 2: # base cases: fib(0)=0, fib(1)=1
+ return n
+ return fib(n - 1) + fib(n - 2)
+
+for i in range(7):
+ print(fib(i), end=" ")
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python fibonacci.py
+0 1 1 2 3 5 8
+```
+
+:::tip
+Naive recursive Fibonacci recomputes the same values many times and is slow for large `n`. Cache results with `functools.lru_cache` to make it fast: add `@lru_cache` above the function.
+:::
+
+## Recursion with a Collection: Summing a List
+Recursion also works on collections — handle the first item, then recurse on the rest.
+
+```python title="sum_list.py" showLineNumbers{1} {1-7}
+def sum_list(items):
+ if not items: # base case: empty list
+ return 0
+ return items[0] + sum_list(items[1:]) # first + sum of the rest
+
+print(sum_list([1, 2, 3, 4, 5]))
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python sum_list.py
+15
+```
+
+## The Recursion Limit
+Python limits how deep recursion can go (default around 1000 calls) to prevent a stack overflow from crashing the interpreter. You can inspect and change it.
+
+```python title="limit.py" showLineNumbers{1} {1-4}
+import sys
+print(sys.getrecursionlimit()) # default: 1000
+sys.setrecursionlimit(2000) # raise it (use with care)
+```
+
+:::caution
+Raising the recursion limit too high can crash Python with a real stack overflow. If you need very deep recursion, rewrite the algorithm iteratively instead.
+:::
+
+## Recursion vs. Iteration
+Any recursive solution can be rewritten as a loop, and vice versa. The factorial as a loop:
+
+```python title="factorial_iterative.py" showLineNumbers{1} {1-6}
+def factorial(n):
+ result = 1
+ for i in range(2, n + 1):
+ result *= i
+ return result
+
+print(factorial(5)) # 120
+```
+
+| Recursion | Iteration |
+| :--- | :--- |
+| Often clearer for self-similar problems (trees, divide & conquer) | Often faster and uses less memory |
+| Uses the call stack — limited depth | No depth limit beyond available memory |
+| Elegant, fewer lines | More explicit state management |
+
+## Conclusion
+Recursion solves a problem by having a function call itself on a smaller input until it reaches a base case. Always define a base case to stop the recursion, and ensure each recursive call moves toward it. Classic examples include factorials, Fibonacci numbers, and list traversals. Remember Python's recursion depth limit, cache repeated work with `lru_cache`, and consider iteration when performance or depth matters. For more hands-on examples and in-depth tutorials, explore our resources on Python Central Hub!
+
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+## Try it: Recursion Exercises
+
+### Exercise 1 – Factorial
+
+
+
+### Exercise 2 – Sum a List Recursively
+
+
+
+### Exercise 3 – Fibonacci
+
+
diff --git a/src/content/docs/tutorials/Python Iterators and Generators/Iterators and Generators.mdx b/src/content/docs/tutorials/Python Iterators and Generators/Iterators and Generators.mdx
new file mode 100644
index 00000000..8eb34bab
--- /dev/null
+++ b/src/content/docs/tutorials/Python Iterators and Generators/Iterators and Generators.mdx
@@ -0,0 +1,341 @@
+---
+title: Iterators and Generators in Python
+description: Learn about iterators and generators in Python. Understand the iterator protocol with __iter__ and __next__, build your own iterators, and use the yield keyword and generator expressions to produce values lazily and save memory.
+sidebar:
+ order: 99
+---
+
+## Mastering Iterators and Generators in Python: A Comprehensive Guide
+Whenever you write a `for` loop over a list, string, or file, Python is using an **iterator** behind the scenes. Iterators let you walk through a collection one element at a time without loading everything into memory. **Generators** are a simple, elegant way to build iterators using the `yield` keyword. In this guide, we'll explore the iterator protocol, build custom iterators, and master generators and generator expressions.
+
+## What is an Iterable?
+An **iterable** is any object you can loop over — lists, tuples, strings, dictionaries, sets, and files are all iterables. An iterable can be passed to the built-in `iter()` function to get an **iterator**.
+
+```python title="iterable.py" showLineNumbers{1} {1-3}
+numbers = [1, 2, 3]
+for n in numbers: # 'numbers' is an iterable
+ print(n)
+```
+
+## What is an Iterator?
+An **iterator** is an object that produces the next value each time you call `next()` on it. An iterator remembers its position. It follows the **iterator protocol**: it implements two methods, `__iter__()` and `__next__()`.
+
+```python title="iterator.py" showLineNumbers{1} {1-6}
+numbers = [10, 20, 30]
+it = iter(numbers) # get an iterator from the iterable
+print(next(it)) # 10
+print(next(it)) # 20
+print(next(it)) # 30
+print(next(it)) # raises StopIteration
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-7}
+C:\Users\Your Name> python iterator.py
+10
+20
+30
+Traceback (most recent call last):
+StopIteration
+```
+
+When there are no more values, the iterator raises `StopIteration`. A `for` loop catches this exception automatically and stops looping.
+
+**Diagram**:
+```mermaid title="iterator protocol" desc="How a for loop uses the iterator protocol"
+graph TD
+ A[Start for loop] --> B[Call iter on iterable]
+ B --> C[Call next on iterator]
+ C --> D{StopIteration?}
+ D -->|No| E[Run loop body]
+ E --> C
+ D -->|Yes| F[End loop]
+```
+
+## Iterable vs. Iterator
+| Concept | Has `__iter__()` | Has `__next__()` | Example |
+| :--- | :---: | :---: | :--- |
+| Iterable | Yes | No | `list`, `str`, `dict` |
+| Iterator | Yes | Yes | result of `iter(list)` |
+
+:::note
+Every iterator is also an iterable (its `__iter__` returns itself), but not every iterable is an iterator. A `list` is iterable but not an iterator — you cannot call `next()` on it directly.
+:::
+
+## Building a Custom Iterator
+To create your own iterator class, implement `__iter__()` (which returns the iterator object, usually `self`) and `__next__()` (which returns the next value or raises `StopIteration`).
+
+```python title="custom_iterator.py" showLineNumbers{1} {1-17}
+class CountUpTo:
+ """Iterator that yields numbers from 1 up to a limit."""
+ def __init__(self, limit):
+ self.limit = limit
+ self.current = 0
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ if self.current >= self.limit:
+ raise StopIteration
+ self.current += 1
+ return self.current
+
+for number in CountUpTo(3):
+ print(number)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-4}
+C:\Users\Your Name> python custom_iterator.py
+1
+2
+3
+```
+
+## What is a Generator?
+A **generator** is a special function that returns an iterator. Instead of `return`, it uses the `yield` keyword. Each time `yield` runs, the function's state is frozen and a value is produced; execution resumes from that point on the next `next()` call.
+
+The syntax of a generator function in Python is as follows:
+
+```python title="Syntax" showLineNumbers{1} {1-3}
+def generator_function():
+ yield value1
+ yield value2
+```
+
+The same `CountUpTo` logic as a generator — far shorter:
+
+```python title="generator.py" showLineNumbers{1} {1-8}
+def count_up_to(limit):
+ current = 1
+ while current <= limit:
+ yield current
+ current += 1
+
+for number in count_up_to(3):
+ print(number)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-4}
+C:\Users\Your Name> python generator.py
+1
+2
+3
+```
+
+:::tip
+A function is a generator if it contains *at least one* `yield` statement. Calling it does not run the body — it returns a generator object immediately. The body runs only as you iterate.
+:::
+
+## yield vs. return
+- `return` sends back a single value and ends the function permanently.
+- `yield` produces a value and *pauses* the function, keeping its local state so it can resume later.
+
+```python title="yield_pause.py" showLineNumbers{1} {1-10}
+def demo():
+ print("start")
+ yield 1
+ print("resumed")
+ yield 2
+ print("done")
+
+gen = demo()
+print(next(gen)) # prints 'start' then 1
+print(next(gen)) # prints 'resumed' then 2
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-5}
+C:\Users\Your Name> python yield_pause.py
+start
+1
+resumed
+2
+```
+
+## Why Generators Save Memory
+A generator produces values **lazily** — one at a time, only when requested. This means it never builds the whole sequence in memory. Compare creating a list of one million squares versus a generator:
+
+```python title="memory.py" showLineNumbers{1} {1-9}
+import sys
+
+# List: stores all million values at once
+squares_list = [x * x for x in range(1_000_000)]
+print("list bytes:", sys.getsizeof(squares_list))
+
+# Generator: stores almost nothing
+squares_gen = (x * x for x in range(1_000_000))
+print("generator bytes:", sys.getsizeof(squares_gen))
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-3}
+C:\Users\Your Name> python memory.py
+list bytes: 8448728
+generator bytes: 208
+```
+
+## Generator Expressions
+A **generator expression** looks like a list comprehension but uses parentheses `()` instead of square brackets `[]`. It produces a generator instead of a list.
+
+```python title="genexpr.py" showLineNumbers{1} {1-5}
+# List comprehension - builds a list
+squares = [x * x for x in range(5)]
+
+# Generator expression - lazy, no list built
+lazy_squares = (x * x for x in range(5))
+
+print(sum(x * x for x in range(5))) # pass directly to sum()
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python genexpr.py
+30
+```
+
+:::tip
+When a generator expression is the only argument to a function, you can drop the extra parentheses: `sum(x*x for x in range(5))`.
+:::
+
+## Infinite Sequences
+Because generators are lazy, they can represent **infinite** sequences — something impossible with a list.
+
+```python title="infinite.py" showLineNumbers{1} {1-10}
+def naturals():
+ n = 1
+ while True: # never stops on its own
+ yield n
+ n += 1
+
+gen = naturals()
+for value in gen:
+ if value > 5:
+ break
+ print(value)
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2-6}
+C:\Users\Your Name> python infinite.py
+1
+2
+3
+4
+5
+```
+
+## Conclusion
+Iterators are objects that produce values one at a time via `__iter__()` and `__next__()`, and they power every `for` loop in Python. Generators give you the same behaviour with far less code through the `yield` keyword, while generator expressions offer a compact, memory-efficient alternative to list comprehensions. Because they produce values lazily, generators can handle huge or even infinite sequences with almost no memory. For more hands-on examples and in-depth tutorials, explore our resources on Python Central Hub!
+
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+## Try it: Iterators and Generators Exercises
+
+### Exercise 1 – Using iter() and next()
+
+ should print red
+print(___(it)) # replace ___ -> should print green
+
+# -- Expected Output --------------------------------------------
+# red
+# green
+# ---------------------------------------------------------------`}
+ solution={`colors = ["red", "green", "blue"]
+it = iter(colors)
+print(next(it))
+print(next(it))`}
+ sct={`test_output_contains("red")
+test_output_contains("green")
+success_msg("You used the iterator protocol!")`}
+ height={200}
+/>
+
+### Exercise 2 – Write a Generator Function
+
+
+
+### Exercise 3 – Generator Expression
+
+
diff --git a/src/content/docs/tutorials/Python Standard Library/Argparse and sys.argv.mdx b/src/content/docs/tutorials/Python Standard Library/Argparse and sys.argv.mdx
new file mode 100644
index 00000000..b1b7b584
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/Argparse and sys.argv.mdx
@@ -0,0 +1,216 @@
+---
+title: Python argparse & sys.argv — CLI Arguments
+description: A detailed guide to building command-line interfaces in Python with sys.argv and argparse. Learn positional and optional arguments, types, defaults, flags, and help text — with examples and practice exercises.
+sidebar:
+ order: 10
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+When you run a script from the terminal, the words after the script name are **command-line arguments**. Python gives you two ways to read them: the raw `sys.argv` list, and the full-featured `argparse` library.
+
+```python title="run_example.py"
+# $ python greet.py Ada --times 3
+import sys
+print(sys.argv) # ['greet.py', 'Ada', '--times', '3']
+```
+
+## sys.argv — the raw list
+
+`sys.argv` is a list of strings. The first item is always the script name; the rest are the arguments, **as strings**.
+
+```python title="sys_argv.py"
+import sys
+
+# $ python add.py 3 4
+if len(sys.argv) != 3:
+ print("Usage: python add.py NUM NUM")
+ sys.exit(1)
+
+a = int(sys.argv[1]) # convert from string!
+b = int(sys.argv[2])
+print(a + b) # 7
+```
+
+`sys.argv` is fine for one or two arguments, but it forces you to handle conversion, validation, defaults, and help text by hand. For anything real, use `argparse`.
+
+## argparse — the proper way
+
+`argparse` parses arguments, converts types, generates `--help`, and reports errors automatically.
+
+```python title="argparse_basic.py"
+import argparse
+
+parser = argparse.ArgumentParser(description="Greet someone.")
+parser.add_argument("name", help="who to greet")
+parser.add_argument("--times", type=int, default=1, help="how many times")
+
+args = parser.parse_args() # reads from sys.argv
+for _ in range(args.times):
+ print(f"Hello, {args.name}!")
+```
+
+Running it:
+
+```bash title="terminal"
+$ python greet.py Ada --times 2
+Hello, Ada!
+Hello, Ada!
+
+$ python greet.py --help
+usage: greet.py [-h] [--times TIMES] name
+...
+```
+
+## Positional vs optional arguments
+
+| Kind | Defined as | Example |
+|:---|:---|:---|
+| **Positional** | `add_argument("name")` | `python app.py Ada` |
+| **Optional** | `add_argument("--times")` | `python app.py --times 3` |
+| **Flag** | `add_argument("--verbose", action="store_true")` | `python app.py --verbose` |
+
+```python title="positional_optional.py"
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("filename") # required positional
+parser.add_argument("--limit", type=int, default=10) # optional with default
+parser.add_argument("--verbose", action="store_true")# flag -> True/False
+
+# parse_args can take an explicit list (handy for testing)
+args = parser.parse_args(["data.txt", "--limit", "5", "--verbose"])
+print(args.filename) # data.txt
+print(args.limit) # 5
+print(args.verbose) # True
+```
+
+> Tip: `parse_args()` reads `sys.argv` by default, but you can pass a list — `parse_args(["a", "b"])` — which is perfect for tests and examples.
+
+## Useful add_argument options
+
+| Option | Effect |
+|:---|:---|
+| `type=int` | Convert the value (to int, float, etc.). |
+| `default=...` | Value used when the argument is omitted. |
+| `required=True` | Make an optional argument mandatory. |
+| `choices=[...]` | Restrict to a fixed set of values. |
+| `action="store_true"` | Boolean flag (no value needed). |
+| `nargs="+"` | Accept multiple values into a list. |
+| `help="..."` | Text shown in `--help`. |
+
+```python title="advanced.py"
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--mode", choices=["fast", "safe"], default="safe")
+parser.add_argument("--files", nargs="+") # one or more values
+args = parser.parse_args(["--mode", "fast", "--files", "a.txt", "b.txt"])
+print(args.mode) # fast
+print(args.files) # ['a.txt', 'b.txt']
+```
+
+## Short flags
+
+Add a one-letter alias alongside the long name:
+
+```python title="short_flags.py"
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("-n", "--number", type=int, default=1)
+args = parser.parse_args(["-n", "7"])
+print(args.number) # 7
+```
+
+## Common pitfalls
+
+- **`sys.argv` values are strings** — convert with `int()`/`float()`.
+- **`sys.argv[0]` is the script name**, not the first argument.
+- **Dashes become underscores** — `--max-size` is read as `args.max_size`.
+- **`store_true` flags default to `False`** — presence flips them to `True`.
+- **argparse exits on bad input** — it prints usage and calls `sys.exit`, which is usually what you want.
+
+## Practice Exercises
+
+### Exercise 1 – Read from a simulated argv
+
+
+
+### Exercise 2 – Parse a positional argument
+
+
+
+### Exercise 3 – An optional integer with a default
+
+ use the default
+print(args.count)
+
+# Expected Output:
+# 5`}
+ solution={`import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--count", type=int, default=5)
+args = parser.parse_args([])
+print(args.count)`}
+ sct={`test_output_contains("5")
+success_msg("With no args supplied, argparse uses the default.")`}
+ height={190}
+/>
+
+## Summary
+
+- `sys.argv` is the raw list of string arguments (`argv[0]` is the script name).
+- `argparse` adds types, defaults, choices, flags, validation, and auto-generated `--help`.
+- Positional args are required; `--optional` args can have defaults; `store_true` makes boolean flags.
+- Pass an explicit list to `parse_args([...])` for testing and examples.
diff --git a/src/content/docs/tutorials/Python Standard Library/CSV.mdx b/src/content/docs/tutorials/Python Standard Library/CSV.mdx
new file mode 100644
index 00000000..dbd98e22
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/CSV.mdx
@@ -0,0 +1,233 @@
+---
+title: Python csv — Read & Write CSV
+description: A detailed guide to Python's csv module. Learn to read and write CSV files with reader, writer, DictReader, and DictWriter, handle headers and dialects, with examples and practice exercises.
+sidebar:
+ order: 8
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+**CSV** (Comma-Separated Values) is the universal format for tabular data — spreadsheets, exports, datasets. The `csv` module reads and writes it correctly, handling quoting, commas inside fields, and newlines that naive string splitting would get wrong.
+
+```python title="quickstart.py"
+import csv
+import io
+
+# Read rows from CSV text
+text = "name,age\nAda,36\nBob,25"
+reader = csv.reader(io.StringIO(text))
+for row in reader:
+ print(row)
+# ['name', 'age']
+# ['Ada', '36']
+# ['Bob', '25']
+```
+
+> **Always open files with `newline=""`** when using `csv`. This lets the module handle line endings itself and prevents blank rows on Windows.
+
+## The four tools
+
+| Tool | Direction | Row shape |
+|:---|:---|:---|
+| `csv.reader(file)` | Read | Each row is a **list**. |
+| `csv.writer(file)` | Write | You pass **lists**. |
+| `csv.DictReader(file)` | Read | Each row is a **dict** keyed by header. |
+| `csv.DictWriter(file, fieldnames)` | Write | You pass **dicts**. |
+
+## Writing with csv.writer
+
+```python title="writer.py"
+import csv
+
+rows = [
+ ["name", "city"],
+ ["Ada", "London"],
+ ["Bob", "New York, NY"], # comma inside a field is handled automatically
+]
+
+with open("people.csv", "w", newline="", encoding="utf-8") as f:
+ writer = csv.writer(f)
+ writer.writerow(["title", "year"]) # one row
+ writer.writerows(rows) # many rows
+```
+
+The module **quotes** fields that contain commas, quotes, or newlines, so `"New York, NY"` stays a single field.
+
+## Reading with csv.reader
+
+```python title="reader.py"
+import csv
+
+with open("people.csv", "r", newline="", encoding="utf-8") as f:
+ reader = csv.reader(f)
+ header = next(reader) # pull the header row off first
+ print("Header:", header)
+ for row in reader:
+ print(row) # each row is a list of strings
+```
+
+> Every value read from CSV is a **string**. Convert numbers yourself: `int(row[1])`, `float(row[2])`.
+
+## DictReader — rows as dictionaries
+
+`DictReader` uses the first row as keys, so you access columns by name instead of position.
+
+```python title="dictreader.py"
+import csv
+import io
+
+text = "name,age,city\nAda,36,London\nBob,25,NYC"
+reader = csv.DictReader(io.StringIO(text))
+for row in reader:
+ print(row["name"], "is", row["age"])
+# Ada is 36
+# Bob is 25
+```
+
+## DictWriter — write dictionaries
+
+`DictWriter` needs `fieldnames`; call `writeheader()` to emit the header row.
+
+```python title="dictwriter.py"
+import csv
+
+people = [
+ {"name": "Ada", "age": 36},
+ {"name": "Bob", "age": 25},
+]
+
+with open("out.csv", "w", newline="", encoding="utf-8") as f:
+ writer = csv.DictWriter(f, fieldnames=["name", "age"])
+ writer.writeheader()
+ writer.writerows(people)
+```
+
+## Custom delimiters and dialects
+
+Not every "CSV" uses commas. Pass `delimiter` for TSV (tabs), semicolons, or pipes.
+
+```python title="delimiters.py"
+import csv
+import io
+
+tsv = "name\tage\nAda\t36"
+reader = csv.reader(io.StringIO(tsv), delimiter="\t")
+print(list(reader)) # [['name', 'age'], ['Ada', '36']]
+```
+
+| Parameter | Controls |
+|:---|:---|
+| `delimiter` | Field separator (default `,`). |
+| `quotechar` | Character used to quote fields (default `"`). |
+| `quoting` | When to quote (`csv.QUOTE_MINIMAL`, `QUOTE_ALL`, ...). |
+| `escapechar` | Escape character for special chars. |
+
+## Common pitfalls
+
+- **Missing `newline=""`** — causes blank rows between every record on Windows.
+- **Forgetting values are strings** — `row["age"] + 1` fails; convert with `int(...)` first.
+- **`DictWriter` without `writeheader()`** — produces a file with no header row.
+- **Mismatched `fieldnames`** — a dict key not in `fieldnames` raises `ValueError`.
+
+## Practice Exercises
+
+### Exercise 1 – Parse CSV text
+
+
+
+### Exercise 2 – Sum a numeric column
+
+
+
+### Exercise 3 – Write rows to CSV text
+
+
+
+## Summary
+
+- The `csv` module handles quoting and embedded commas/newlines that manual splitting breaks.
+- Use `reader`/`writer` for lists, `DictReader`/`DictWriter` for dicts keyed by header.
+- Open files with `newline=""` and an explicit `encoding`.
+- Remember every read value is a **string** — convert numbers yourself.
+- Set `delimiter` for TSV and other separated formats.
diff --git a/src/content/docs/tutorials/Python Standard Library/Collections.mdx b/src/content/docs/tutorials/Python Standard Library/Collections.mdx
new file mode 100644
index 00000000..6555c4b4
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/Collections.mdx
@@ -0,0 +1,261 @@
+---
+title: Python collections — Counter, defaultdict, namedtuple, deque
+description: A detailed guide to Python's collections module. Learn Counter, defaultdict, namedtuple, deque, OrderedDict, and ChainMap with examples and practice exercises.
+sidebar:
+ order: 4
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+The `collections` module provides **specialized container types** that go beyond the built-in `list`, `dict`, `tuple`, and `set`. They make common tasks — counting, grouping, fixed-field records, and fast queues — shorter and faster.
+
+```python title="overview.py"
+from collections import Counter, defaultdict, namedtuple, deque
+
+print(Counter("banana")) # Counter({'a': 3, 'n': 2, 'b': 1})
+print(defaultdict(list)) # defaultdict(, {})
+Point = namedtuple("Point", "x y")
+print(Point(1, 2)) # Point(x=1, y=2)
+print(deque([1, 2, 3])) # deque([1, 2, 3])
+```
+
+| Type | Use it for |
+|:---|:---|
+| `Counter` | Counting hashable items (tallies, frequencies). |
+| `defaultdict` | A dict that auto-creates default values for missing keys. |
+| `namedtuple` | Lightweight, immutable records with named fields. |
+| `deque` | Fast appends/pops at **both** ends (queues, stacks). |
+| `OrderedDict` | A dict with extra order-related methods. |
+| `ChainMap` | Search multiple dicts as one. |
+
+## Counter
+
+`Counter` is a `dict` subclass for counting. Pass any iterable and it tallies occurrences.
+
+```python title="counter.py"
+from collections import Counter
+
+votes = ["a", "b", "a", "c", "a", "b"]
+c = Counter(votes)
+print(c) # Counter({'a': 3, 'b': 2, 'c': 1})
+print(c["a"]) # 3
+print(c["z"]) # 0 (missing keys return 0, never KeyError)
+
+# Most common items
+print(c.most_common(2)) # [('a', 3), ('b', 2)]
+
+# Counters do arithmetic
+c2 = Counter("aab")
+print(c + c2) # combine counts
+print(c - c2) # subtract counts
+
+# Update with more data
+c.update(["a", "d"])
+print(c["a"], c["d"]) # 4 1
+```
+
+| Method | Returns |
+|:---|:---|
+| `c.most_common(n)` | The `n` highest-count `(item, count)` pairs. |
+| `c.elements()` | An iterator repeating each element by its count. |
+| `c.update(iterable)` | Add counts from another iterable/Counter. |
+| `c.total()` | Sum of all counts (Python 3.10+). |
+
+## defaultdict
+
+A regular dict raises `KeyError` for a missing key. A `defaultdict` instead calls a **factory** function to create a default value automatically — perfect for grouping and accumulating.
+
+```python title="defaultdict.py"
+from collections import defaultdict
+
+# Group words by their first letter
+words = ["apple", "banana", "avocado", "cherry", "blueberry"]
+groups = defaultdict(list)
+for w in words:
+ groups[w[0]].append(w)
+print(dict(groups))
+# {'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry'], 'c': ['cherry']}
+
+# Count with an int factory (default 0)
+counts = defaultdict(int)
+for ch in "mississippi":
+ counts[ch] += 1
+print(dict(counts)) # {'m': 1, 'i': 4, 's': 4, 'p': 2}
+```
+
+The factory can be `list`, `int`, `set`, `dict`, or any zero-argument callable.
+
+## namedtuple
+
+`namedtuple` creates a tuple subclass with **named fields** — readable, immutable records without writing a full class.
+
+```python title="namedtuple.py"
+from collections import namedtuple
+
+Point = namedtuple("Point", ["x", "y"])
+p = Point(3, 4)
+
+print(p.x, p.y) # 3 4 (access by name)
+print(p[0], p[1]) # 3 4 (still a tuple — index access works)
+
+# Unpacking works too
+x, y = p
+print(x, y) # 3 4
+
+# Handy helpers
+print(p._asdict()) # {'x': 3, 'y': 4}
+p2 = p._replace(y=10) # returns a NEW point (immutable)
+print(p2) # Point(x=3, y=10)
+```
+
+Use a namedtuple when you have a small fixed set of fields and want clarity over a bare tuple or dict.
+
+## deque
+
+A `deque` ("double-ended queue") supports **O(1)** appends and pops from both ends — unlike a list, which is slow at the front.
+
+```python title="deque.py"
+from collections import deque
+
+dq = deque([1, 2, 3])
+dq.append(4) # add right -> deque([1, 2, 3, 4])
+dq.appendleft(0) # add left -> deque([0, 1, 2, 3, 4])
+print(dq.pop()) # 4 (remove right)
+print(dq.popleft()) # 0 (remove left)
+print(dq) # deque([1, 2, 3])
+
+# Fixed-size deque: old items drop off automatically
+last3 = deque(maxlen=3)
+for i in range(6):
+ last3.append(i)
+print(last3) # deque([3, 4, 5], maxlen=3)
+
+# Rotate
+d = deque([1, 2, 3, 4])
+d.rotate(1) # deque([4, 1, 2, 3])
+print(d)
+```
+
+| Method | Effect |
+|:---|:---|
+| `append(x)` / `appendleft(x)` | Add to the right / left. |
+| `pop()` / `popleft()` | Remove from the right / left. |
+| `extend(it)` / `extendleft(it)` | Add many to the right / left. |
+| `rotate(n)` | Rotate items `n` steps to the right. |
+| `maxlen` | Optional fixed length; overflow drops the opposite end. |
+
+## OrderedDict and ChainMap (briefly)
+
+```python title="others.py"
+from collections import OrderedDict, ChainMap
+
+# OrderedDict: insertion order is preserved (regular dicts do this too since 3.7),
+# but it adds move_to_end() and an order-sensitive ==.
+od = OrderedDict([("a", 1), ("b", 2)])
+od.move_to_end("a")
+print(list(od)) # ['b', 'a']
+
+# ChainMap: search several dicts as one, first match wins.
+defaults = {"color": "red", "size": "M"}
+overrides = {"color": "blue"}
+settings = ChainMap(overrides, defaults)
+print(settings["color"]) # blue (overrides win)
+print(settings["size"]) # M (falls back to defaults)
+```
+
+## Common pitfalls
+
+- **`defaultdict` creates keys on access** — even just reading `d[missing]` inserts it. Use `d.get(k)` if you don't want that.
+- **namedtuples are immutable** — `_replace` returns a new instance; it doesn't mutate.
+- **Use `deque` for queues**, not `list.pop(0)`, which is O(n).
+- **`Counter` values can go negative** after subtraction; use `+c` to drop non-positive counts.
+
+## Practice Exercises
+
+### Exercise 1 – Count characters
+
+
+
+### Exercise 2 – Group numbers by even/odd
+
+
+
+### Exercise 3 – Keep the last 3 items
+
+
+
+## Summary
+
+- **`Counter`** tallies items and ranks them with `most_common`.
+- **`defaultdict`** removes `KeyError` boilerplate for grouping/accumulating.
+- **`namedtuple`** gives readable, immutable records with named fields.
+- **`deque`** is the right tool for fast double-ended queues and bounded buffers.
+- **`OrderedDict`** and **`ChainMap`** cover order-sensitive and layered-lookup needs.
diff --git a/src/content/docs/tutorials/Python Standard Library/Date and Time.mdx b/src/content/docs/tutorials/Python Standard Library/Date and Time.mdx
new file mode 100644
index 00000000..9f24276c
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/Date and Time.mdx
@@ -0,0 +1,305 @@
+---
+title: Python Date & Time — datetime, time, calendar
+description: A detailed guide to Python's datetime, time, and calendar modules. Learn to create, format, parse, and do arithmetic with dates and times, handle time zones, and work with calendars — with examples and practice exercises.
+sidebar:
+ order: 2
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+Python ships three complementary modules for working with dates and times:
+
+- **`datetime`** — high-level objects for dates, times, and durations. This is what you reach for most.
+- **`time`** — lower-level, OS-facing functions (timestamps, sleeping, performance timers).
+- **`calendar`** — calendar-related helpers (leap years, weekday names, month grids).
+
+```python title="overview.py"
+import datetime, time, calendar
+
+print(datetime.date.today()) # 2025-01-31
+print(time.time()) # 1738291200.123 (seconds since epoch)
+print(calendar.isleap(2024)) # True
+```
+
+## The `datetime` module
+
+It defines four key classes:
+
+| Class | Represents |
+|:---|:---|
+| `date` | A calendar date — year, month, day. |
+| `time` | A time of day — hour, minute, second, microsecond. |
+| `datetime` | A date **and** a time combined. |
+| `timedelta` | A **duration** — the difference between two dates/times. |
+
+### Creating dates and times
+
+```python title="creating.py"
+from datetime import date, time, datetime
+
+d = date(2025, 1, 31)
+print(d.year, d.month, d.day) # 2025 1 31
+
+t = time(14, 30, 15)
+print(t.hour, t.minute) # 14 30
+
+dt = datetime(2025, 1, 31, 14, 30, 15)
+print(dt) # 2025-01-31 14:30:15
+
+# "Now" helpers
+print(date.today()) # today's date
+print(datetime.now()) # local date + time right now
+```
+
+### Accessing parts
+
+```python title="attributes.py"
+from datetime import datetime
+
+dt = datetime(2025, 1, 31, 14, 30, 15)
+print(dt.date()) # 2025-01-31
+print(dt.time()) # 14:30:15
+print(dt.weekday()) # 4 (Mon=0 ... Sun=6)
+print(dt.isoweekday()) # 5 (Mon=1 ... Sun=7)
+print(dt.isoformat()) # 2025-01-31T14:30:15
+```
+
+## Formatting with `strftime`
+
+`strftime` ("string format time") turns a date/time **into** a string using format codes.
+
+| Code | Meaning | Example |
+|:---|:---|:---|
+| `%Y` | 4-digit year | 2025 |
+| `%y` | 2-digit year | 25 |
+| `%m` | Month, zero-padded | 01 |
+| `%B` | Full month name | January |
+| `%b` | Abbreviated month | Jan |
+| `%d` | Day of month | 31 |
+| `%A` | Full weekday name | Friday |
+| `%a` | Abbreviated weekday | Fri |
+| `%H` | Hour (24-hour) | 14 |
+| `%I` | Hour (12-hour) | 02 |
+| `%M` | Minute | 30 |
+| `%S` | Second | 15 |
+| `%p` | AM / PM | PM |
+| `%j` | Day of the year | 031 |
+
+```python title="strftime.py"
+from datetime import datetime
+
+dt = datetime(2025, 1, 31, 14, 30, 15)
+print(dt.strftime("%Y-%m-%d")) # 2025-01-31
+print(dt.strftime("%A, %B %d, %Y")) # Friday, January 31, 2025
+print(dt.strftime("%I:%M %p")) # 02:30 PM
+```
+
+## Parsing with `strptime`
+
+`strptime` ("string parse time") does the reverse — it reads a string **into** a `datetime`, given a format that describes its layout.
+
+```python title="strptime.py"
+from datetime import datetime
+
+s = "2025-01-31 14:30"
+dt = datetime.strptime(s, "%Y-%m-%d %H:%M")
+print(dt) # 2025-01-31 14:30:00
+print(type(dt)) #
+
+# ISO strings have a dedicated fast parser:
+print(datetime.fromisoformat("2025-01-31T14:30:15"))
+```
+
+## Date arithmetic with `timedelta`
+
+A `timedelta` is a span of time. Add or subtract it from dates; subtract two dates to get a `timedelta`.
+
+```python title="timedelta.py"
+from datetime import date, datetime, timedelta
+
+today = date(2025, 1, 31)
+week_later = today + timedelta(days=7)
+print(week_later) # 2025-02-07
+
+start = datetime(2025, 1, 1, 9, 0)
+end = datetime(2025, 1, 1, 17, 30)
+worked = end - start
+print(worked) # 8:30:00
+print(worked.total_seconds()) # 30600.0
+
+# timedelta accepts weeks, days, hours, minutes, seconds
+print(timedelta(weeks=2, days=3)) # 17 days, 0:00:00
+```
+
+## Time zones (aware vs naive)
+
+A datetime with no time zone is **naive**; one carrying `tzinfo` is **aware**. Prefer aware datetimes for anything crossing zones. Since Python 3.9 the standard library includes `zoneinfo`.
+
+```python title="timezones.py"
+from datetime import datetime, timezone, timedelta
+from zoneinfo import ZoneInfo
+
+# UTC-aware "now"
+now_utc = datetime.now(timezone.utc)
+print(now_utc.tzinfo) # UTC
+
+# Convert between zones
+ny = datetime(2025, 1, 31, 12, 0, tzinfo=ZoneInfo("America/New_York"))
+tokyo = ny.astimezone(ZoneInfo("Asia/Tokyo"))
+print(tokyo) # 2025-02-01 02:00:00+09:00
+```
+
+## The `time` module
+
+Lower-level utilities tied to the operating system clock.
+
+| Function | Purpose |
+|:---|:---|
+| `time.time()` | Seconds since the epoch (a float timestamp). |
+| `time.sleep(secs)` | Pause execution. |
+| `time.localtime()` | Convert a timestamp to a local `struct_time`. |
+| `time.gmtime()` | Convert a timestamp to a UTC `struct_time`. |
+| `time.strftime(fmt, t)` | Format a `struct_time`. |
+| `time.perf_counter()` | High-resolution timer for benchmarking. |
+
+```python title="time_module.py"
+import time
+
+start = time.perf_counter()
+time.sleep(0.1) # pause 100 ms
+elapsed = time.perf_counter() - start
+print(f"Slept for {elapsed:.3f}s") # Slept for 0.100s
+
+ts = time.time()
+print(time.strftime("%Y-%m-%d", time.localtime(ts)))
+```
+
+> Use `perf_counter()` to **measure durations**, and `time()` to get a **timestamp**. Never use `time()` for benchmarking — it can jump if the system clock changes.
+
+## The `calendar` module
+
+Helpers for reasoning about months, weeks, and leap years.
+
+| Function | Returns |
+|:---|:---|
+| `calendar.isleap(year)` | `True` if the year is a leap year. |
+| `calendar.monthrange(y, m)` | `(first_weekday, num_days)` for that month. |
+| `calendar.weekday(y, m, d)` | Weekday index (Mon=0). |
+| `calendar.month(y, m)` | A text calendar for the month. |
+| `calendar.day_name[i]` | Full weekday name for index `i`. |
+
+```python title="calendar_module.py"
+import calendar
+
+print(calendar.isleap(2024)) # True
+print(calendar.monthrange(2025, 2)) # (5, 28) -> starts Sat, 28 days
+print(calendar.day_name[0]) # Monday
+
+# Print a whole month grid
+print(calendar.month(2025, 1))
+```
+
+## Putting it together
+
+```python title="age_in_days.py"
+from datetime import date
+
+def days_until_birthday(birth_month, birth_day):
+ today = date.today()
+ this_year = date(today.year, birth_month, birth_day)
+ next_bday = this_year if this_year >= today else date(today.year + 1, birth_month, birth_day)
+ return (next_bday - today).days
+
+print(days_until_birthday(12, 25)) # days until the next Dec 25
+```
+
+## Common pitfalls
+
+- **Mixing naive and aware datetimes** raises `TypeError` when you subtract them. Pick one and stick with it.
+- **`weekday()` vs `isoweekday()`** — one starts Monday at 0, the other at 1.
+- **`strptime` format must match exactly**, including separators, or it raises `ValueError`.
+- **`time.sleep` blocks the whole thread** — fine for scripts, but use async patterns in servers.
+
+## Practice Exercises
+
+### Exercise 1 – Format today's date
+
+
+
+### Exercise 2 – Days between two dates
+
+
+
+### Exercise 3 – Is it a leap year?
+
+
+
+## Summary
+
+- Use **`datetime`** for dates, times, durations (`timedelta`), and arithmetic.
+- **Format** with `strftime`, **parse** with `strptime` / `fromisoformat`.
+- Prefer **aware** datetimes (`timezone.utc`, `zoneinfo.ZoneInfo`) when zones matter.
+- **`time`** gives timestamps, sleeping, and `perf_counter` for benchmarks.
+- **`calendar`** answers leap-year, weekday, and month-grid questions.
diff --git a/src/content/docs/tutorials/Python Standard Library/Itertools and Functools.mdx b/src/content/docs/tutorials/Python Standard Library/Itertools and Functools.mdx
new file mode 100644
index 00000000..9de2cc11
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/Itertools and Functools.mdx
@@ -0,0 +1,270 @@
+---
+title: Python itertools & functools
+description: A detailed guide to Python's itertools and functools modules. Learn iterator building blocks (chain, product, combinations, groupby, accumulate) and higher-order helpers (reduce, lru_cache, partial, wraps) with examples and practice exercises.
+sidebar:
+ order: 5
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+These two modules power Python's **functional** toolkit:
+
+- **`itertools`** — fast, memory-efficient building blocks for working with iterators.
+- **`functools`** — higher-order functions that act on or return other functions (caching, partial application, reduction).
+
+```python title="overview.py"
+import itertools, functools
+
+print(list(itertools.chain([1, 2], [3, 4]))) # [1, 2, 3, 4]
+print(functools.reduce(lambda a, b: a + b, [1, 2, 3, 4])) # 10
+```
+
+## itertools
+
+Iterators produce values **lazily** — one at a time, without building a whole list in memory. Many `itertools` tools return infinite or large iterators, so wrap them in `list(...)` or `islice(...)` to inspect them.
+
+### Infinite iterators
+
+| Function | Produces |
+|:---|:---|
+| `count(start, step)` | `start, start+step, ...` forever. |
+| `cycle(iterable)` | Repeats the iterable endlessly. |
+| `repeat(x, n)` | `x`, `n` times (or forever if `n` omitted). |
+
+```python title="infinite.py"
+from itertools import count, cycle, repeat, islice
+
+print(list(islice(count(10, 2), 4))) # [10, 12, 14, 16]
+print(list(islice(cycle("AB"), 5))) # ['A', 'B', 'A', 'B', 'A']
+print(list(repeat("x", 3))) # ['x', 'x', 'x']
+```
+
+### Combining and slicing iterators
+
+| Function | Effect |
+|:---|:---|
+| `chain(a, b, ...)` | Concatenate iterables end to end. |
+| `islice(it, stop)` | Slice an iterator like a list. |
+| `compress(data, sel)` | Keep items where the selector is truthy. |
+| `zip_longest(a, b)` | Like `zip` but pads the shorter one. |
+
+```python title="combining.py"
+from itertools import chain, islice, compress, zip_longest
+
+print(list(chain([1, 2], [3], [4, 5]))) # [1, 2, 3, 4, 5]
+print(list(compress("ABCD", [1, 0, 1, 0]))) # ['A', 'C']
+print(list(zip_longest([1, 2, 3], ["a"], fillvalue="-")))
+# [(1, 'a'), (2, '-'), (3, '-')]
+```
+
+### Combinatorics
+
+| Function | Produces |
+|:---|:---|
+| `product(a, b)` | Cartesian product (nested loops). |
+| `permutations(it, r)` | All ordered arrangements of length `r`. |
+| `combinations(it, r)` | All unordered selections of length `r`. |
+| `combinations_with_replacement(it, r)` | Selections allowing repeats. |
+
+```python title="combinatorics.py"
+from itertools import product, permutations, combinations
+
+print(list(product([1, 2], ["a", "b"])))
+# [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
+print(list(permutations([1, 2, 3], 2)))
+# [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
+print(list(combinations([1, 2, 3], 2)))
+# [(1, 2), (1, 3), (2, 3)]
+```
+
+### Grouping and accumulating
+
+```python title="group_accumulate.py"
+from itertools import groupby, accumulate
+
+# groupby groups CONSECUTIVE equal keys -> sort first if needed
+data = [("fruit", "apple"), ("fruit", "pear"), ("veg", "kale")]
+for key, items in groupby(data, key=lambda pair: pair[0]):
+ print(key, [name for _, name in items])
+# fruit ['apple', 'pear']
+# veg ['kale']
+
+# accumulate yields running totals (or any binary function)
+print(list(accumulate([1, 2, 3, 4]))) # [1, 3, 6, 10]
+print(list(accumulate([1, 2, 3, 4], max))) # [1, 2, 3, 4]
+```
+
+## functools
+
+### reduce
+
+`reduce` folds an iterable into a single value by repeatedly applying a two-argument function.
+
+```python title="reduce.py"
+from functools import reduce
+
+print(reduce(lambda a, b: a + b, [1, 2, 3, 4])) # 10
+print(reduce(lambda a, b: a * b, [1, 2, 3, 4])) # 24
+print(reduce(lambda a, b: a if a > b else b, [3, 9, 2])) # 9 (max)
+```
+
+### lru_cache and cache — memoization
+
+`@lru_cache` stores recent results so repeated calls with the same arguments are instant. Great for expensive or recursive functions.
+
+```python title="lru_cache.py"
+from functools import lru_cache
+
+@lru_cache(maxsize=None) # unbounded cache
+def fib(n):
+ if n < 2:
+ return n
+ return fib(n - 1) + fib(n - 2)
+
+print(fib(30)) # 832040 — fast, thanks to caching
+print(fib.cache_info()) # CacheInfo(hits=..., misses=..., ...)
+```
+
+> In Python 3.9+, `@functools.cache` is a simpler alias for `@lru_cache(maxsize=None)`.
+
+### partial — pre-fill arguments
+
+`partial` creates a new function with some arguments already supplied.
+
+```python title="partial.py"
+from functools import partial
+
+def power(base, exp):
+ return base ** exp
+
+square = partial(power, exp=2)
+cube = partial(power, exp=3)
+print(square(5)) # 25
+print(cube(2)) # 8
+
+# Common in callbacks: partial(int, base=2) parses binary
+to_binary = partial(int, base=2)
+print(to_binary("1010")) # 10
+```
+
+### wraps — preserve metadata in decorators
+
+When you write a decorator, `@wraps` copies the original function's name and docstring onto the wrapper.
+
+```python title="wraps.py"
+from functools import wraps
+
+def shout(func):
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ return func(*args, **kwargs).upper()
+ return wrapper
+
+@shout
+def greet(name):
+ "Return a greeting."
+ return f"hello {name}"
+
+print(greet("ada")) # HELLO ADA
+print(greet.__name__) # greet (preserved by @wraps)
+print(greet.__doc__) # Return a greeting.
+```
+
+| functools tool | Purpose |
+|:---|:---|
+| `reduce(func, it)` | Fold an iterable to one value. |
+| `lru_cache` / `cache` | Memoize function results. |
+| `partial(func, *args)` | Bind some arguments ahead of time. |
+| `wraps` | Keep metadata when writing decorators. |
+| `cmp_to_key` | Adapt an old-style comparison function for `sorted`. |
+| `total_ordering` | Fill in missing comparison methods on a class. |
+
+## Common pitfalls
+
+- **Iterators are single-use** — once consumed, they're empty. Re-create them to iterate again.
+- **`groupby` only groups adjacent equal keys** — sort by the key first if groups aren't contiguous.
+- **Infinite iterators** (`count`, `cycle`) will hang `list()` — slice them with `islice`.
+- **`lru_cache` requires hashable arguments** — you can't cache calls that take lists or dicts.
+
+## Practice Exercises
+
+### Exercise 1 – Running totals
+
+
+
+### Exercise 2 – Multiply a list with reduce
+
+
+
+### Exercise 3 – Pre-fill an argument with partial
+
+
+
+## Summary
+
+- **`itertools`** offers lazy building blocks: infinite generators, combiners (`chain`, `zip_longest`), combinatorics (`product`, `permutations`, `combinations`), and `groupby`/`accumulate`.
+- Slice infinite iterators with `islice`; sort before `groupby`.
+- **`functools`** adds `reduce`, memoization (`lru_cache`/`cache`), argument binding (`partial`), and decorator helpers (`wraps`).
+- These tools make data pipelines shorter, faster, and clearer.
diff --git a/src/content/docs/tutorials/Python Standard Library/JSON.mdx b/src/content/docs/tutorials/Python Standard Library/JSON.mdx
new file mode 100644
index 00000000..8bcc83e2
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/JSON.mdx
@@ -0,0 +1,267 @@
+---
+title: Python json — Parse & Serialize JSON
+description: A detailed guide to Python's json module. Learn to serialize Python objects to JSON and parse JSON back into Python, with dumps/loads/dump/load, formatting options, custom encoders, and practice exercises.
+sidebar:
+ order: 3
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+**JSON** (JavaScript Object Notation) is the most common format for exchanging data between programs, APIs, and config files. Python's built-in `json` module converts between JSON text and Python objects — no installation needed.
+
+```python title="quickstart.py"
+import json
+
+data = {"name": "Ada", "age": 36, "skills": ["math", "code"]}
+text = json.dumps(data) # Python -> JSON string
+print(text) # {"name": "Ada", "age": 36, "skills": ["math", "code"]}
+
+back = json.loads(text) # JSON string -> Python
+print(back["skills"][0]) # math
+```
+
+## The four functions
+
+The module has two pairs: one works with **strings**, the other with **files**.
+
+| Function | Direction | Works with |
+|:---|:---|:---|
+| `json.dumps(obj)` | Python → JSON | A **string** ("dump string"). |
+| `json.loads(text)` | JSON → Python | A **string** ("load string"). |
+| `json.dump(obj, file)` | Python → JSON | A **file** object. |
+| `json.load(file)` | JSON → Python | A **file** object. |
+
+Remember the **s**: `dumps`/`loads` end in **s** for **string**; `dump`/`load` are for files.
+
+## How types map
+
+Serialization converts Python types to their JSON equivalents (and back):
+
+| Python | JSON |
+|:---|:---|
+| `dict` | object |
+| `list`, `tuple` | array |
+| `str` | string |
+| `int`, `float` | number |
+| `True` / `False` | `true` / `false` |
+| `None` | `null` |
+
+> Note: tuples become JSON arrays, and JSON arrays always load back as **lists** — so a round-trip turns a tuple into a list.
+
+## Serializing: `dumps`
+
+```python title="dumps.py"
+import json
+
+data = {"name": "Ada", "active": True, "score": None, "tags": ["a", "b"]}
+
+# Compact (default)
+print(json.dumps(data))
+
+# Pretty-printed with indentation
+print(json.dumps(data, indent=2))
+
+# Sort keys alphabetically
+print(json.dumps(data, sort_keys=True))
+
+# Custom separators for the most compact output
+print(json.dumps(data, separators=(",", ":")))
+```
+
+Common `dumps` / `dump` options:
+
+| Option | Effect |
+|:---|:---|
+| `indent=n` | Pretty-print with `n` spaces per level. |
+| `sort_keys=True` | Order object keys alphabetically. |
+| `separators=(item, key)` | Control the item and key-value separators. |
+| `ensure_ascii=False` | Keep non-ASCII characters as-is (e.g. emoji, accents). |
+| `default=func` | A fallback function to serialize unknown types. |
+
+```python title="unicode.py"
+import json
+
+data = {"city": "São Paulo", "emoji": "🐍"}
+print(json.dumps(data)) # escapes non-ASCII
+print(json.dumps(data, ensure_ascii=False)) # keeps São Paulo and 🐍 readable
+```
+
+## Parsing: `loads`
+
+```python title="loads.py"
+import json
+
+text = '{"name": "Ada", "age": 36, "skills": ["math", "code"], "active": true}'
+obj = json.loads(text)
+
+print(type(obj)) #
+print(obj["name"]) # Ada
+print(obj["active"]) # True (JSON true -> Python True)
+print(obj["skills"][1]) # code
+```
+
+## Working with files
+
+Use `dump`/`load` to read and write JSON files directly.
+
+```python title="files.py"
+import json
+
+config = {"theme": "dark", "version": 2, "plugins": ["a", "b"]}
+
+# Write to a file
+with open("config.json", "w", encoding="utf-8") as f:
+ json.dump(config, f, indent=2)
+
+# Read it back
+with open("config.json", "r", encoding="utf-8") as f:
+ loaded = json.load(f)
+
+print(loaded["theme"]) # dark
+```
+
+## Handling errors
+
+Invalid JSON raises `json.JSONDecodeError` (a subclass of `ValueError`). Always guard parsing of untrusted input.
+
+```python title="errors.py"
+import json
+
+bad = '{"name": "Ada", }' # trailing comma is invalid JSON
+
+try:
+ json.loads(bad)
+except json.JSONDecodeError as e:
+ print("Could not parse:", e)
+```
+
+## Serializing custom objects
+
+By default, `json` only knows the built-in types. For your own classes, pass a `default` function (or convert to a dict first).
+
+```python title="custom.py"
+import json
+from datetime import datetime
+
+def encode(obj):
+ if isinstance(obj, datetime):
+ return obj.isoformat()
+ raise TypeError(f"Cannot serialize {type(obj).__name__}")
+
+event = {"name": "launch", "when": datetime(2025, 1, 31, 9, 0)}
+print(json.dumps(event, default=encode))
+# {"name": "launch", "when": "2025-01-31T09:00:00"}
+```
+
+## Practical example — a tiny API response
+
+```python title="api_like.py"
+import json
+
+response = '''
+{
+ "status": "ok",
+ "users": [
+ {"id": 1, "name": "Ada"},
+ {"id": 2, "name": "Linus"}
+ ]
+}
+'''
+
+data = json.loads(response)
+for user in data["users"]:
+ print(user["id"], "->", user["name"])
+# 1 -> Ada
+# 2 -> Linus
+```
+
+## Common pitfalls
+
+- **Single quotes** — JSON requires **double** quotes around keys and strings. `"{'a': 1}"` fails; use `'{"a": 1}'`.
+- **Trailing commas** are invalid in JSON (unlike Python).
+- **`True`/`None` vs `true`/`null`** — Python prints capitalized; JSON uses lowercase. The module handles the conversion for you, but watch for it when hand-writing JSON.
+- **Non-serializable types** (sets, datetimes, custom classes) raise `TypeError` unless you supply `default`.
+- **Keys become strings** — JSON object keys are always strings, so `{1: "a"}` serializes to `{"1": "a"}`.
+
+## Practice Exercises
+
+### Exercise 1 – Serialize a dictionary
+
+
+
+### Exercise 2 – Parse JSON and read a value
+
+
+
+### Exercise 3 – Pretty-print with indentation
+
+
+
+## Summary
+
+- `json` converts between JSON text and Python objects with **four functions**: `dumps`/`loads` (strings) and `dump`/`load` (files).
+- Python types map cleanly to JSON; tuples and JSON arrays both become **lists** on the way back.
+- Format output with `indent`, `sort_keys`, `separators`, and `ensure_ascii`.
+- Guard parsing with `try/except json.JSONDecodeError`.
+- Serialize custom types with a `default` function.
diff --git a/src/content/docs/tutorials/Python Standard Library/Logging.mdx b/src/content/docs/tutorials/Python Standard Library/Logging.mdx
new file mode 100644
index 00000000..cf552c95
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/Logging.mdx
@@ -0,0 +1,224 @@
+---
+title: Python logging — Structured Application Logs
+description: A detailed guide to Python's logging module. Learn log levels, basic configuration, formatters, handlers, and per-module loggers — with examples and practice exercises.
+sidebar:
+ order: 9
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+The `logging` module is the standard way to record what your program is doing. Unlike `print`, it has **severity levels**, can route messages to files or the network, includes timestamps and source info, and can be turned up or down without touching your code.
+
+```python title="quickstart.py"
+import logging
+
+logging.basicConfig(level=logging.INFO)
+logging.info("App started")
+logging.warning("Low disk space")
+logging.error("Connection failed")
+# INFO:root:App started
+# WARNING:root:Low disk space
+# ERROR:root:Connection failed
+```
+
+## Why not just use print?
+
+| `print` | `logging` |
+|:---|:---|
+| One firehose to stdout. | Severity levels you can filter. |
+| No timestamps or context. | Timestamps, module, line number, etc. |
+| Edit code to silence it. | Change one config line. |
+| Goes one place. | Files, console, syslog, email — many handlers. |
+
+## Log levels
+
+Each message has a level. The logger only emits messages **at or above** its configured level.
+
+| Level | Value | Use for |
+|:---|:---|:---|
+| `DEBUG` | 10 | Detailed diagnostic info. |
+| `INFO` | 20 | Normal events ("server started"). |
+| `WARNING` | 30 | Something unexpected, but still working (the default). |
+| `ERROR` | 40 | A failure in some operation. |
+| `CRITICAL` | 50 | A serious failure; the program may stop. |
+
+```python title="levels.py"
+import logging
+
+logging.basicConfig(level=logging.DEBUG)
+logging.debug("variable x = 42") # shown only because level is DEBUG
+logging.info("processing record")
+logging.warning("retrying")
+logging.error("gave up")
+print(logging.getLevelName(20)) # INFO
+```
+
+> The default level is `WARNING`, so `debug` and `info` messages are **hidden** until you lower the level with `basicConfig(level=...)`.
+
+## basicConfig — quick setup
+
+`basicConfig` configures the root logger in one call. Set it **once**, early, before logging anything.
+
+```python title="basic_config.py"
+import logging
+
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
+ datefmt="%H:%M:%S",
+)
+logging.info("ready")
+# 14:30:01 [INFO] root: ready
+
+# Write to a file instead of the console
+logging.basicConfig(filename="app.log", level=logging.INFO)
+```
+
+### Common format placeholders
+
+| Placeholder | Inserts |
+|:---|:---|
+| `%(asctime)s` | Human-readable timestamp. |
+| `%(levelname)s` | The level name (INFO, ERROR, ...). |
+| `%(name)s` | The logger's name. |
+| `%(message)s` | The log message. |
+| `%(filename)s` / `%(lineno)d` | Source file / line number. |
+
+## Named loggers — one per module
+
+Instead of the root logger, create a logger per module with `getLogger(__name__)`. This tags every message with where it came from and lets you tune modules independently.
+
+```python title="named_loggers.py"
+import logging
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+logger.info("module-specific message")
+# INFO:__main__:module-specific message
+```
+
+## Handlers and formatters
+
+For real apps, attach **handlers** (where logs go) and **formatters** (how they look). You can have several at once — e.g. INFO to console, ERROR to a file.
+
+```python title="handlers.py"
+import logging
+
+logger = logging.getLogger("myapp")
+logger.setLevel(logging.DEBUG)
+
+# Console handler at INFO
+console = logging.StreamHandler()
+console.setLevel(logging.INFO)
+console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
+
+# File handler at DEBUG
+file = logging.FileHandler("debug.log")
+file.setLevel(logging.DEBUG)
+
+logger.addHandler(console)
+logger.addHandler(file)
+
+logger.debug("only in the file")
+logger.info("console + file")
+```
+
+## Logging exceptions
+
+Inside an `except` block, `logger.exception(...)` logs the message **plus the full traceback**.
+
+```python title="exceptions.py"
+import logging
+
+logging.basicConfig(level=logging.ERROR)
+try:
+ 1 / 0
+except ZeroDivisionError:
+ logging.exception("Math went wrong")
+# ERROR:root:Math went wrong
+# Traceback (most recent call last): ...
+```
+
+## Common pitfalls
+
+- **Default level hides INFO/DEBUG** — call `basicConfig(level=logging.DEBUG)` to see them.
+- **`basicConfig` only works once** — the first call wins; later calls are ignored unless `force=True`.
+- **Use `%s` lazy formatting**: `logger.info("user %s", name)`, not f-strings, so the string is only built if the message is actually emitted.
+- **Don't log secrets** — passwords, tokens, and personal data should never hit the logs.
+
+## Practice Exercises
+
+### Exercise 1 – Level names and values
+
+
+
+### Exercise 2 – Configure and log info
+
+
+
+### Exercise 3 – Create a named logger
+
+
+
+## Summary
+
+- `logging` replaces `print` with leveled, timestamped, routable messages.
+- Levels: `DEBUG < INFO < WARNING < ERROR < CRITICAL`; default is `WARNING`.
+- Configure quickly with `basicConfig(level=..., format=...)`.
+- Use `getLogger(__name__)` per module, and attach handlers/formatters for fine control.
+- Log tracebacks with `logger.exception` inside `except` blocks.
diff --git a/src/content/docs/tutorials/Python Standard Library/Math Random Statistics.mdx b/src/content/docs/tutorials/Python Standard Library/Math Random Statistics.mdx
new file mode 100644
index 00000000..c40b7cda
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/Math Random Statistics.mdx
@@ -0,0 +1,230 @@
+---
+title: Python math, random & statistics
+description: A detailed guide to Python's math, random, and statistics modules. Learn numeric functions and constants, random number generation and sampling, and descriptive statistics with examples and practice exercises.
+sidebar:
+ order: 6
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+Three modules cover everyday numeric work:
+
+- **`math`** — mathematical functions and constants for real numbers.
+- **`random`** — pseudo-random numbers, choices, shuffling, and sampling.
+- **`statistics`** — descriptive statistics (mean, median, standard deviation).
+
+```python title="overview.py"
+import math, random, statistics
+
+print(math.sqrt(144)) # 12.0
+print(random.randint(1, 6)) # a dice roll, e.g. 4
+print(statistics.mean([2, 4, 6])) # 4
+```
+
+## The math module
+
+### Constants
+
+| Constant | Value |
+|:---|:---|
+| `math.pi` | 3.141592653589793 |
+| `math.e` | 2.718281828459045 |
+| `math.tau` | 6.283185307179586 (2π) |
+| `math.inf` | Positive infinity |
+| `math.nan` | Not-a-Number |
+
+### Common functions
+
+| Function | Returns |
+|:---|:---|
+| `math.sqrt(x)` | Square root. |
+| `math.pow(x, y)` | `x` to the power `y` (as a float). |
+| `math.floor(x)` / `math.ceil(x)` | Round down / up to an integer. |
+| `math.trunc(x)` | Drop the fractional part. |
+| `math.factorial(n)` | `n!` |
+| `math.gcd(a, b)` / `math.lcm(a, b)` | Greatest common divisor / least common multiple. |
+| `math.exp(x)` / `math.log(x, base)` | e^x / logarithm. |
+| `math.isclose(a, b)` | Safe float comparison. |
+| `math.comb(n, k)` / `math.perm(n, k)` | Combinations / permutations count. |
+
+```python title="math_funcs.py"
+import math
+
+print(math.floor(3.7), math.ceil(3.2)) # 3 4
+print(math.factorial(5)) # 120
+print(math.gcd(12, 18)) # 6
+print(math.log(8, 2)) # 3.0
+print(math.comb(5, 2)) # 10
+
+# Never compare floats with == ; use isclose
+print(0.1 + 0.2 == 0.3) # False (float rounding!)
+print(math.isclose(0.1 + 0.2, 0.3)) # True
+```
+
+### Trigonometry
+
+Trig functions work in **radians**. Convert with `radians` / `degrees`.
+
+```python title="trig.py"
+import math
+
+print(math.sin(math.pi / 2)) # 1.0
+print(math.degrees(math.pi)) # 180.0
+print(math.radians(180)) # 3.141592653589793
+print(math.hypot(3, 4)) # 5.0 (Euclidean distance)
+```
+
+## The random module
+
+`random` generates **pseudo-random** values. For reproducible results (tests, demos) set a seed first.
+
+| Function | Returns |
+|:---|:---|
+| `random.random()` | A float in `[0.0, 1.0)`. |
+| `random.uniform(a, b)` | A float in `[a, b]`. |
+| `random.randint(a, b)` | An integer in `[a, b]` (inclusive). |
+| `random.randrange(stop)` | An integer like `range`. |
+| `random.choice(seq)` | One random element. |
+| `random.choices(seq, k=n)` | `n` elements **with** replacement (weights allowed). |
+| `random.sample(seq, k=n)` | `n` unique elements **without** replacement. |
+| `random.shuffle(list)` | Shuffle a list **in place**. |
+| `random.seed(n)` | Make results reproducible. |
+
+```python title="random_funcs.py"
+import random
+
+random.seed(42) # reproducible output
+
+print(random.random()) # 0.6394...
+print(random.randint(1, 6)) # dice roll
+print(random.choice(["a", "b", "c"])) # one element
+
+deck = [1, 2, 3, 4, 5]
+random.shuffle(deck) # shuffles in place
+print(deck)
+
+print(random.sample(range(1, 50), 6)) # lottery: 6 unique numbers
+print(random.choices(["heads", "tails"], weights=[1, 1], k=3))
+```
+
+> `random` is **not** cryptographically secure. For passwords, tokens, or keys, use the `secrets` module instead.
+
+## The statistics module
+
+Descriptive statistics on numeric data, no third-party libraries required.
+
+| Function | Returns |
+|:---|:---|
+| `statistics.mean(data)` | Arithmetic average. |
+| `statistics.median(data)` | Middle value. |
+| `statistics.mode(data)` | Most common value. |
+| `statistics.stdev(data)` | Sample standard deviation. |
+| `statistics.pstdev(data)` | Population standard deviation. |
+| `statistics.variance(data)` | Sample variance. |
+| `statistics.harmonic_mean(data)` | Harmonic mean. |
+
+```python title="stats.py"
+import statistics
+
+scores = [88, 92, 79, 93, 85, 92]
+print(statistics.mean(scores)) # 88.16...
+print(statistics.median(scores)) # 90.0
+print(statistics.mode(scores)) # 92
+print(round(statistics.stdev(scores), 2)) # 5.34
+print(statistics.variance(scores)) # 28.57...
+```
+
+## Putting it together
+
+```python title="dice_simulation.py"
+import random
+import statistics
+
+random.seed(1)
+rolls = [random.randint(1, 6) for _ in range(1000)]
+print("mean:", round(statistics.mean(rolls), 2)) # close to 3.5
+print("mode:", statistics.mode(rolls))
+```
+
+## Common pitfalls
+
+- **Float `==` is unreliable** — `0.1 + 0.2 != 0.3`. Use `math.isclose`.
+- **Trig uses radians**, not degrees — convert with `math.radians`.
+- **`random.shuffle` returns `None`** — it shuffles in place; don't write `x = random.shuffle(x)`.
+- **`statistics.mode` errors on ties** in older versions; `multimode` returns all top values.
+- **Use `secrets`, not `random`,** for anything security-sensitive.
+
+## Practice Exercises
+
+### Exercise 1 – Square root and rounding
+
+
+
+### Exercise 2 – Reproducible dice roll
+
+
+
+### Exercise 3 – Average of a list
+
+
+
+## Summary
+
+- **`math`** provides constants (`pi`, `e`, `tau`) and functions for roots, logs, factorials, gcd/lcm, trig, and safe float comparison (`isclose`).
+- **`random`** generates pseudo-random numbers and supports `choice`, `choices`, `sample`, and `shuffle`; seed it for reproducibility and use `secrets` for security.
+- **`statistics`** computes mean, median, mode, variance, and standard deviation without external libraries.
diff --git a/src/content/docs/tutorials/Python Standard Library/Pathlib.mdx b/src/content/docs/tutorials/Python Standard Library/Pathlib.mdx
new file mode 100644
index 00000000..5a81490b
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/Pathlib.mdx
@@ -0,0 +1,222 @@
+---
+title: Python pathlib — Modern File Paths
+description: A detailed guide to Python's pathlib module. Learn to build, inspect, and manipulate filesystem paths with the Path object, read and write files, and glob directories — with examples and practice exercises.
+sidebar:
+ order: 11
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+`pathlib` provides an **object-oriented** way to work with filesystem paths. Instead of juggling strings and `os.path.join`, you use a `Path` object with clean attributes and methods that work the same on Windows, macOS, and Linux.
+
+```python title="quickstart.py"
+from pathlib import Path
+
+p = Path("data") / "reports" / "2025.csv" # join with the / operator
+print(p) # data/reports/2025.csv (uses the OS separator)
+print(p.name) # 2025.csv
+print(p.suffix) # .csv
+print(p.parent) # data/reports
+```
+
+## Why pathlib over os.path?
+
+| `os.path` (old) | `pathlib` (modern) |
+|:---|:---|
+| `os.path.join(a, b)` | `Path(a) / b` |
+| `os.path.basename(p)` | `p.name` |
+| `os.path.splitext(p)[1]` | `p.suffix` |
+| `os.path.exists(p)` | `p.exists()` |
+| Strings everywhere. | One tidy object. |
+
+## Building paths
+
+Use the `/` operator to join parts — it's readable and cross-platform.
+
+```python title="building.py"
+from pathlib import Path
+
+base = Path("/home/user")
+config = base / "app" / "config.json"
+print(config) # /home/user/app/config.json
+
+# Current working directory and home directory
+print(Path.cwd()) # where the script runs
+print(Path.home()) # the user's home folder
+```
+
+## Inspecting a path
+
+| Attribute / method | Returns |
+|:---|:---|
+| `p.name` | Final component (`2025.csv`). |
+| `p.stem` | Name without suffix (`2025`). |
+| `p.suffix` | The extension (`.csv`). |
+| `p.parent` | The containing directory. |
+| `p.parts` | A tuple of all components. |
+| `p.with_suffix(".txt")` | A new path with a changed extension. |
+| `p.with_name("new.csv")` | A new path with a changed final name. |
+
+```python title="inspecting.py"
+from pathlib import Path
+
+p = Path("reports/q1/sales.csv")
+print(p.name) # sales.csv
+print(p.stem) # sales
+print(p.suffix) # .csv
+print(p.parent) # reports/q1
+print(p.parts) # ('reports', 'q1', 'sales.csv')
+print(p.with_suffix(".json")) # reports/q1/sales.json
+```
+
+## Checking existence and type
+
+```python title="checks.py"
+from pathlib import Path
+
+p = Path("notes.txt")
+print(p.exists()) # True if the path exists
+print(p.is_file()) # True if it's a file
+print(p.is_dir()) # True if it's a directory
+```
+
+## Reading and writing files
+
+`pathlib` can read and write whole files in one line — no `open()` boilerplate for simple cases.
+
+```python title="read_write.py"
+from pathlib import Path
+
+p = Path("greeting.txt")
+p.write_text("Hello, file!", encoding="utf-8") # write (and create)
+print(p.read_text(encoding="utf-8")) # Hello, file!
+
+# Binary variants
+p.write_bytes(b"\x00\x01")
+print(p.read_bytes()) # b'\x00\x01'
+```
+
+## Listing and globbing directories
+
+```python title="listing.py"
+from pathlib import Path
+
+folder = Path(".")
+
+# Everything directly inside
+for item in folder.iterdir():
+ print(item)
+
+# Only .py files in this folder
+for py in folder.glob("*.py"):
+ print(py)
+
+# Recursively, every .py file under here
+for py in folder.rglob("*.py"):
+ print(py)
+```
+
+| Method | Finds |
+|:---|:---|
+| `p.iterdir()` | Every item directly inside `p`. |
+| `p.glob(pattern)` | Items matching a pattern in `p`. |
+| `p.rglob(pattern)` | Matches **recursively** in all subfolders. |
+
+## Creating and removing
+
+```python title="create_remove.py"
+from pathlib import Path
+
+d = Path("output/logs")
+d.mkdir(parents=True, exist_ok=True) # create folders; don't error if present
+
+f = d / "run.log"
+f.touch() # create an empty file
+f.unlink(missing_ok=True) # delete a file (no error if missing)
+```
+
+## Common pitfalls
+
+- **`Path("a") / "b"` only works with a Path on the left** — `"a" / Path("b")` fails; start from a `Path`.
+- **`p.suffix` includes the dot** — `.csv`, not `csv`.
+- **`mkdir` errors if the parent is missing** — pass `parents=True`.
+- **`read_text` raises if the file doesn't exist** — check `p.exists()` first or catch the error.
+- **Convert to `str(p)`** only when a library insists on a string.
+
+## Practice Exercises
+
+### Exercise 1 – Extract the file extension
+
+
+
+### Exercise 2 – Join paths with the / operator
+
+
+
+### Exercise 3 – Name without the extension
+
+
+
+## Summary
+
+- `pathlib.Path` is the modern, object-oriented replacement for `os.path`.
+- Join paths with `/`; inspect them with `name`, `stem`, `suffix`, `parent`, `parts`.
+- Read/write files directly with `read_text`/`write_text` (and `_bytes` variants).
+- List and search folders with `iterdir`, `glob`, and `rglob`.
+- Create and remove with `mkdir(parents=True, exist_ok=True)`, `touch`, and `unlink`.
diff --git a/src/content/docs/tutorials/Python Standard Library/Pickle.mdx b/src/content/docs/tutorials/Python Standard Library/Pickle.mdx
new file mode 100644
index 00000000..706fa344
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/Pickle.mdx
@@ -0,0 +1,210 @@
+---
+title: Python pickle — Object Serialization
+description: A detailed guide to Python's pickle module. Learn to serialize and deserialize Python objects with dumps/loads/dump/load, understand what is picklable, pickle vs JSON, and the critical security warning — with examples and practice exercises.
+sidebar:
+ order: 13
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+`pickle` converts almost any Python object into a **byte stream** (serializing / "pickling") and back again (deserializing / "unpickling"). Use it to save program state, cache results, or pass objects between Python processes.
+
+```python title="quickstart.py"
+import pickle
+
+data = {"name": "Ada", "scores": [88, 92], "active": True}
+
+blob = pickle.dumps(data) # object -> bytes
+restored = pickle.loads(blob) # bytes -> object
+print(restored) # {'name': 'Ada', 'scores': [88, 92], 'active': True}
+print(restored == data) # True
+```
+
+## Security warning — read this first
+
+> **Never unpickle data you do not trust.** Unpickling can execute arbitrary code, so a malicious pickle file can take over your program. Only load pickles you created yourself or received from a trusted source. For untrusted or cross-language data, use **JSON** instead.
+
+## The four functions
+
+Like `json`, `pickle` has a string/bytes pair and a file pair.
+
+| Function | Direction | Works with |
+|:---|:---|:---|
+| `pickle.dumps(obj)` | Object → bytes | A bytes object. |
+| `pickle.loads(bytes)` | bytes → Object | A bytes object. |
+| `pickle.dump(obj, file)` | Object → bytes | A **binary** file. |
+| `pickle.load(file)` | bytes → Object | A **binary** file. |
+
+## Saving to and loading from a file
+
+Pickle writes **binary**, so open files in binary mode (`"wb"` / `"rb"`).
+
+```python title="files.py"
+import pickle
+
+model = {"weights": [0.1, 0.2, 0.3], "bias": 0.5}
+
+# Save
+with open("model.pkl", "wb") as f:
+ pickle.dump(model, f)
+
+# Load
+with open("model.pkl", "rb") as f:
+ loaded = pickle.load(f)
+
+print(loaded["bias"]) # 0.5
+```
+
+## What can be pickled?
+
+Pickle handles far more than JSON: nearly all built-in types, plus custom class instances.
+
+| Picklable | Not picklable |
+|:---|:---|
+| `int`, `float`, `str`, `bool`, `None` | Open file handles |
+| `list`, `tuple`, `dict`, `set` | Network sockets, DB connections |
+| Nested combinations of the above | Lambdas (use a named function) |
+| Instances of your own classes | Generators / running threads |
+
+```python title="custom_objects.py"
+import pickle
+
+class Point:
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+ def __repr__(self):
+ return f"Point({self.x}, {self.y})"
+
+p = Point(3, 4)
+blob = pickle.dumps(p)
+p2 = pickle.loads(blob)
+print(p2) # Point(3, 4)
+print(p2 is p) # False -> a brand-new, equal object
+```
+
+> To unpickle a custom class, its **class definition must be importable** in the program doing the loading.
+
+## Protocols
+
+Pickle has several binary **protocol** versions; higher numbers are more efficient. Use `pickle.HIGHEST_PROTOCOL` for the best, or `pickle.DEFAULT_PROTOCOL` for broad compatibility.
+
+```python title="protocols.py"
+import pickle
+
+data = list(range(1000))
+blob = pickle.dumps(data, protocol=pickle.HIGHEST_PROTOCOL)
+print(len(blob), "bytes")
+print(pickle.loads(blob)[:3]) # [0, 1, 2]
+```
+
+## pickle vs JSON
+
+| | pickle | JSON |
+|:---|:---|:---|
+| Output | Binary, Python-only | Text, language-neutral |
+| Types | Almost any Python object | Basic types only |
+| Human-readable | No | Yes |
+| Safe with untrusted data | **No** | Yes |
+| Best for | Caching/state between Python programs | APIs, configs, data exchange |
+
+Choose **JSON** for data shared with other systems or users; choose **pickle** only for trusted, Python-to-Python data that JSON can't represent.
+
+## Common pitfalls
+
+- **Binary mode required** — open files with `"wb"`/`"rb"`, not `"w"`/`"r"`.
+- **Cross-version risk** — a pickle made in one Python version may not load in another.
+- **Lambdas don't pickle** — use a module-level `def` function instead.
+- **Security** — never `loads`/`load` data from an untrusted source.
+
+## Practice Exercises
+
+### Exercise 1 – Round-trip a dictionary
+
+ loads round-trips the object back to an equal copy.")`}
+ height={190}
+/>
+
+### Exercise 2 – Confirm equality after a round-trip
+
+
+
+### Exercise 3 – Pickle a custom object
+
+
+
+## Summary
+
+- `pickle` serializes almost any Python object to bytes and back.
+- Use `dumps`/`loads` for bytes, `dump`/`load` for **binary** files.
+- It handles custom classes (the class must be importable to unpickle).
+- **Never unpickle untrusted data** — it can run arbitrary code; prefer JSON for shared data.
+- Pick pickle for trusted Python-to-Python state; pick JSON for interoperability.
diff --git a/src/content/docs/tutorials/Python Standard Library/Regular Expressions.mdx b/src/content/docs/tutorials/Python Standard Library/Regular Expressions.mdx
new file mode 100644
index 00000000..8b3955b7
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/Regular Expressions.mdx
@@ -0,0 +1,313 @@
+---
+title: Python re — Regular Expressions
+description: A detailed guide to Python's re module. Learn pattern syntax, metacharacters, special sequences, groups, flags, and the full re API with examples and practice exercises.
+sidebar:
+ order: 1
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+The `re` module is Python's built-in engine for **regular expressions** — compact patterns that describe sets of strings. Use it to search, match, extract, validate, and replace text. It ships with the standard library, so no installation is needed:
+
+```python title="import_re.py"
+import re
+
+text = "Contact: alice@example.com, bob@work.org"
+emails = re.findall(r"[\w.+-]+@[\w-]+\.[\w.-]+", text)
+print(emails)
+# ['alice@example.com', 'bob@work.org']
+```
+
+## Raw strings — always use `r"..."`
+
+Regex patterns use the backslash (`\`) heavily. Python *also* uses backslash for string escapes, so a plain string like `"\b"` becomes a backspace character before `re` ever sees it. **Raw strings** (prefix `r`) turn off Python's escaping so the pattern reaches `re` intact.
+
+```python title="raw_strings.py"
+# Without raw string: Python eats the backslash
+print("\bword") # prints a backspace control char + 'word'
+
+# With raw string: the regex engine receives \b (word boundary)
+import re
+print(re.findall(r"\bword\b", "a word here")) # ['word']
+```
+
+> Rule of thumb: write **every** pattern as a raw string. It costs nothing and avoids subtle bugs.
+
+## The core functions
+
+| Function | What it does |
+|:---|:---|
+| `re.search(pattern, string)` | Scan the whole string; return the **first** match (or `None`). |
+| `re.match(pattern, string)` | Match only at the **start** of the string. |
+| `re.fullmatch(pattern, string)` | Match only if the **entire** string fits the pattern. |
+| `re.findall(pattern, string)` | Return a list of all non-overlapping matches. |
+| `re.finditer(pattern, string)` | Return an iterator of `Match` objects (memory-friendly). |
+| `re.sub(pattern, repl, string)` | Replace all matches with `repl`; returns a new string. |
+| `re.subn(pattern, repl, string)` | Like `sub` but returns `(new_string, count)`. |
+| `re.split(pattern, string)` | Split the string by the pattern. |
+| `re.compile(pattern)` | Pre-compile a pattern into a reusable `Pattern` object. |
+| `re.escape(string)` | Escape all special chars in a literal string. |
+
+```python title="core_functions.py"
+import re
+
+s = "The year 2024 and the year 2025."
+
+print(re.search(r"\d{4}", s).group()) # 2024 (first hit)
+print(re.match(r"\d{4}", s)) # None (string starts with 'The')
+print(re.findall(r"\d{4}", s)) # ['2024', '2025']
+print(re.sub(r"\d{4}", "YEAR", s)) # The year YEAR and the year YEAR.
+print(re.split(r",\s*", "a, b,c, d")) # ['a', 'b', 'c', 'd']
+```
+
+## The Match object
+
+`search`, `match`, `fullmatch`, and each item from `finditer` return a **Match** object. It carries position info and captured groups.
+
+| Method / attribute | Returns |
+|:---|:---|
+| `m.group(0)` / `m.group()` | The whole match. |
+| `m.group(n)` | The text of the nth capture group. |
+| `m.groups()` | A tuple of all groups. |
+| `m.groupdict()` | A dict of all **named** groups. |
+| `m.start()` / `m.end()` | Start / end index of the match. |
+| `m.span()` | `(start, end)` tuple. |
+
+```python title="match_object.py"
+import re
+
+m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "Date: 2025-01-31 ok")
+print(m.group(0)) # 2025-01-31
+print(m.group(1)) # 2025
+print(m.groups()) # ('2025', '01', '31')
+print(m.span()) # (6, 16)
+```
+
+## Metacharacters
+
+These characters have special meaning inside a pattern.
+
+| Token | Meaning |
+|:---|:---|
+| `.` | Any character except newline. |
+| `^` | Start of string (or line, with `re.MULTILINE`). |
+| `$` | End of string (or line, with `re.MULTILINE`). |
+| `*` | 0 or more of the previous token. |
+| `+` | 1 or more of the previous token. |
+| `?` | 0 or 1 (also makes a quantifier lazy). |
+| `{m,n}` | Between m and n repetitions. |
+| `[...]` | A character class — any one char listed. |
+| `[^...]` | Negated class — any char **not** listed. |
+| `\|` | Alternation — "this OR that". |
+| `()` | A capture group. |
+| `\` | Escape a metacharacter, or start a special sequence. |
+
+## Special sequences
+
+| Sequence | Matches |
+|:---|:---|
+| `\d` | A digit (`0-9`). |
+| `\D` | A non-digit. |
+| `\w` | A word char (letters, digits, underscore). |
+| `\W` | A non-word char. |
+| `\s` | Whitespace (space, tab, newline). |
+| `\S` | Non-whitespace. |
+| `\b` | A word boundary. |
+| `\B` | A non-boundary. |
+| `\A` / `\Z` | Start / end of the whole string. |
+
+## Quantifiers — greedy vs lazy
+
+By default quantifiers are **greedy**: they grab as much as possible. Add `?` to make them **lazy** (as little as possible).
+
+```python title="greedy_lazy.py"
+import re
+
+html = ""
+print(re.findall(r"<.+>", html)) # [''] greedy: one big match
+print(re.findall(r"<.+?>", html)) # ['', ''] lazy: smallest matches
+```
+
+## Groups: capturing, named, and non-capturing
+
+```python title="groups.py"
+import re
+
+# Named groups make code self-documenting
+pattern = r"(?P[\w.]+)@(?P[\w.]+)"
+m = re.search(pattern, "send to jane.doe@mail.com please")
+print(m.group("user")) # jane.doe
+print(m.group("domain")) # mail.com
+print(m.groupdict()) # {'user': 'jane.doe', 'domain': 'mail.com'}
+
+# Non-capturing group (?:...) groups without storing a capture
+print(re.findall(r"(?:ab)+", "abababxab")) # ['ababab', 'ab']
+```
+
+You can also reference captured groups in a replacement using `\1`, `\2`, or `\g`:
+
+```python title="sub_backref.py"
+import re
+# Swap "First Last" -> "Last, First"
+print(re.sub(r"(\w+)\s+(\w+)", r"\2, \1", "Ada Lovelace")) # Lovelace, Ada
+```
+
+## Flags
+
+Pass flags to change matching behaviour. Combine them with `|`.
+
+| Flag | Short | Effect |
+|:---|:---|:---|
+| `re.IGNORECASE` | `re.I` | Case-insensitive matching. |
+| `re.MULTILINE` | `re.M` | `^` and `$` match at each line. |
+| `re.DOTALL` | `re.S` | `.` also matches newline. |
+| `re.VERBOSE` | `re.X` | Allow whitespace and comments in the pattern. |
+
+```python title="flags.py"
+import re
+
+print(re.findall(r"cat", "Cat CAT cat", re.IGNORECASE)) # ['Cat', 'CAT', 'cat']
+
+# VERBOSE makes complex patterns readable
+phone = re.compile(r"""
+ (\d{3}) # area code
+ [-.\s]? # optional separator
+ (\d{3}) # prefix
+ [-.\s]?
+ (\d{4}) # line number
+""", re.VERBOSE)
+print(phone.search("Call 415-555-1234").groups()) # ('415', '555', '1234')
+```
+
+## Compile once, reuse many times
+
+When a pattern is used repeatedly (e.g. inside a loop), pre-compile it with `re.compile` for clarity and a small speed gain.
+
+```python title="compile.py"
+import re
+
+word_re = re.compile(r"\b\w+\b")
+for line in ["hello world", "spam eggs"]:
+ print(word_re.findall(line))
+# ['hello', 'world']
+# ['spam', 'eggs']
+```
+
+## Practical examples
+
+```python title="validate.py"
+import re
+
+def is_valid_email(s):
+ return re.fullmatch(r"[\w.+-]+@[\w-]+\.[\w.-]+", s) is not None
+
+print(is_valid_email("user@example.com")) # True
+print(is_valid_email("not-an-email")) # False
+
+# Extract all hashtags
+print(re.findall(r"#(\w+)", "Loving #python and #regex")) # ['python', 'regex']
+
+# Clean up extra whitespace
+print(re.sub(r"\s+", " ", "too many\tspaces").strip()) # 'too many spaces'
+```
+
+## Common pitfalls
+
+- **Forgetting the raw string** — `"\d"` may warn or break; always write `r"\d"`.
+- **Greedy by accident** — `.*` can swallow far more than intended; reach for `.*?` or a tighter class like `[^"]*`.
+- **`match` vs `search`** — `match` anchors at the start. Use `search` to find a pattern anywhere.
+- **Catastrophic backtracking** — nested quantifiers like `(a+)+` on long input can hang. Keep patterns simple.
+
+## Practice Exercises
+
+Try these in the interactive editor. Use the **Hint** and **Show Solution** buttons only after attempting them yourself.
+
+### Exercise 1 – Find all numbers
+
+
+
+### Exercise 2 – Validate a phone number
+
+
+
+### Exercise 3 – Mask sensitive words
+
+
+
+## Summary
+
+- `re` matches, searches, extracts, and replaces text with patterns.
+- Always write patterns as **raw strings** (`r"..."`).
+- Learn the core functions (`search`, `findall`, `finditer`, `sub`, `split`) and the `Match` object.
+- Master metacharacters, special sequences, quantifiers (greedy vs lazy), groups, and flags.
+- Pre-compile hot patterns and keep them simple to avoid backtracking blowups.
diff --git a/src/content/docs/tutorials/Python Standard Library/SQLite3.mdx b/src/content/docs/tutorials/Python Standard Library/SQLite3.mdx
new file mode 100644
index 00000000..fdc09ace
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/SQLite3.mdx
@@ -0,0 +1,270 @@
+---
+title: Python sqlite3 — Database Basics
+description: A detailed guide to Python's sqlite3 module. Learn to connect, create tables, insert, query, parameterize, and commit using SQLite — with examples and practice exercises.
+sidebar:
+ order: 7
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+`sqlite3` is Python's built-in interface to **SQLite**, a tiny, serverless SQL database stored in a single file (or in memory). No setup, no server — perfect for scripts, prototypes, tests, and small apps.
+
+```python title="quickstart.py"
+import sqlite3
+
+conn = sqlite3.connect(":memory:") # database that lives in RAM
+cur = conn.cursor()
+cur.execute("CREATE TABLE users (id INTEGER, name TEXT)")
+cur.execute("INSERT INTO users VALUES (1, 'Ada')")
+conn.commit()
+
+cur.execute("SELECT * FROM users")
+print(cur.fetchall()) # [(1, 'Ada')]
+conn.close()
+```
+
+## The workflow
+
+Every sqlite3 program follows the same shape:
+
+1. **Connect** to a database file (or `:memory:`).
+2. Get a **cursor** to run SQL.
+3. **execute** statements.
+4. **commit** changes that modify data.
+5. **fetch** results from queries.
+6. **close** the connection.
+
+| Object / method | Purpose |
+|:---|:---|
+| `sqlite3.connect(path)` | Open (or create) a database. |
+| `conn.cursor()` | Create a cursor to run statements. |
+| `cur.execute(sql, params)` | Run one SQL statement. |
+| `cur.executemany(sql, seq)` | Run a statement for many rows. |
+| `cur.fetchone()` / `fetchall()` | Get one row / all rows. |
+| `conn.commit()` | Save pending changes. |
+| `conn.close()` | Close the connection. |
+
+## Creating tables and inserting
+
+```python title="create_insert.py"
+import sqlite3
+
+conn = sqlite3.connect("app.db") # creates app.db if missing
+cur = conn.cursor()
+
+cur.execute("""
+ CREATE TABLE IF NOT EXISTS books (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ title TEXT NOT NULL,
+ year INTEGER
+ )
+""")
+
+cur.execute("INSERT INTO books (title, year) VALUES (?, ?)", ("Dune", 1965))
+conn.commit()
+print("Inserted row id:", cur.lastrowid)
+conn.close()
+```
+
+## Parameterized queries — never use string formatting
+
+Always pass values with **`?` placeholders**. Building SQL with f-strings or `+` opens the door to **SQL injection** and quoting bugs.
+
+```python title="parameters.py"
+import sqlite3
+
+conn = sqlite3.connect(":memory:")
+cur = conn.cursor()
+cur.execute("CREATE TABLE users (name TEXT, age INTEGER)")
+
+# SAFE: values are passed separately
+name = "Ada'; DROP TABLE users; --" # malicious input
+cur.execute("INSERT INTO users VALUES (?, ?)", (name, 30))
+
+# Insert many rows at once
+rows = [("Bob", 25), ("Cara", 41)]
+cur.executemany("INSERT INTO users VALUES (?, ?)", rows)
+conn.commit()
+
+cur.execute("SELECT * FROM users WHERE age > ?", (28,))
+print(cur.fetchall()) # [("Ada'; DROP TABLE users; --", 30), ('Cara', 41)]
+conn.close()
+```
+
+> Rule: the SQL string stays constant; **data goes through `?` parameters**.
+
+## Reading results
+
+```python title="reading.py"
+import sqlite3
+
+conn = sqlite3.connect(":memory:")
+cur = conn.cursor()
+cur.execute("CREATE TABLE nums (n INTEGER)")
+cur.executemany("INSERT INTO nums VALUES (?)", [(i,) for i in range(5)])
+conn.commit()
+
+cur.execute("SELECT n FROM nums")
+print(cur.fetchone()) # (0,) -> first row only
+print(cur.fetchall()) # [(1,), (2,), (3,), (4,)] -> remaining rows
+
+# A cursor is iterable — stream rows without loading all at once
+cur.execute("SELECT n FROM nums")
+for (value,) in cur:
+ print(value, end=" ") # 0 1 2 3 4
+conn.close()
+```
+
+### Rows as dictionaries
+
+By default rows are tuples. Set a `row_factory` to access columns by name.
+
+```python title="row_factory.py"
+import sqlite3
+
+conn = sqlite3.connect(":memory:")
+conn.row_factory = sqlite3.Row # rows behave like dicts
+cur = conn.cursor()
+cur.execute("CREATE TABLE p (id INTEGER, name TEXT)")
+cur.execute("INSERT INTO p VALUES (1, 'Ada')")
+
+row = cur.execute("SELECT * FROM p").fetchone()
+print(row["name"]) # Ada
+print(row["id"]) # 1
+conn.close()
+```
+
+## Connections as context managers
+
+Using `with conn:` automatically **commits** on success or **rolls back** on error.
+
+```python title="context_manager.py"
+import sqlite3
+
+conn = sqlite3.connect(":memory:")
+conn.execute("CREATE TABLE t (x INTEGER)")
+
+with conn: # commits automatically
+ conn.execute("INSERT INTO t VALUES (?)", (10,))
+
+print(conn.execute("SELECT * FROM t").fetchall()) # [(10,)]
+conn.close()
+```
+
+## Common pitfalls
+
+- **Forgetting `commit()`** — changes vanish when the program ends. (A `with conn:` block handles this.)
+- **String-built SQL** — always parameterize with `?` to stay safe.
+- **A single value tuple needs a comma** — `(28,)`, not `(28)`.
+- **`:memory:` databases disappear** when the connection closes — fine for tests, not for persistence.
+
+## Practice Exercises
+
+### Exercise 1 – Create and insert
+
+
+
+### Exercise 2 – Parameterized filter
+
+ ?", (___,))
+print(cur.fetchall())
+
+# Expected Output:
+# [('Ada',)]`}
+ solution={`import sqlite3
+
+conn = sqlite3.connect(":memory:")
+cur = conn.cursor()
+cur.execute("CREATE TABLE u (name TEXT, age INTEGER)")
+cur.executemany("INSERT INTO u VALUES (?, ?)", [("Ada", 36), ("Bo", 22)])
+conn.commit()
+
+cur.execute("SELECT name FROM u WHERE age > ?", (30,))
+print(cur.fetchall())`}
+ sct={`test_output_contains("('Ada',)")
+success_msg("The ? placeholder safely passes 30 into the query.")`}
+ height={230}
+/>
+
+### Exercise 3 – Insert many and fetch all
+
+
+
+## Summary
+
+- `sqlite3` is a zero-setup SQL database built into Python.
+- Follow connect → cursor → execute → commit → fetch → close.
+- **Always** parameterize values with `?` to avoid SQL injection.
+- Use `sqlite3.Row` for dict-like rows and `with conn:` for automatic commit/rollback.
diff --git a/src/content/docs/tutorials/Python Standard Library/Testing with unittest and pytest.mdx b/src/content/docs/tutorials/Python Standard Library/Testing with unittest and pytest.mdx
new file mode 100644
index 00000000..bcc9b59d
--- /dev/null
+++ b/src/content/docs/tutorials/Python Standard Library/Testing with unittest and pytest.mdx
@@ -0,0 +1,295 @@
+---
+title: Python Testing — unittest & pytest
+description: A detailed guide to testing Python code with the built-in unittest module and the popular pytest framework. Learn assertions, test cases, fixtures, parametrization, and exception testing — with examples and practice exercises.
+sidebar:
+ order: 12
+---
+
+import DataCampExercise from "../../../../components/DataCampExercise.astro";
+
+Automated tests check that your code does what you expect — and keep doing it as the code changes. Python ships **`unittest`** in the standard library, and the third-party **`pytest`** is the most popular framework in the ecosystem.
+
+```python title="why_test.py"
+def add(a, b):
+ return a + b
+
+# A test is just code that checks an assumption:
+assert add(2, 3) == 5
+assert add(-1, 1) == 0
+print("all good")
+```
+
+## The idea: Arrange, Act, Assert
+
+Most tests follow three steps:
+
+1. **Arrange** — set up inputs and state.
+2. **Act** — call the thing under test.
+3. **Assert** — check the result matches what you expect.
+
+## unittest (standard library)
+
+`unittest` groups tests into classes that subclass `TestCase`. Each method named `test_*` is a test, and you check results with `assert*` methods.
+
+```python title="unittest_basic.py"
+import unittest
+
+def add(a, b):
+ return a + b
+
+class TestAdd(unittest.TestCase):
+ def test_positive(self):
+ self.assertEqual(add(2, 3), 5)
+
+ def test_negative(self):
+ self.assertEqual(add(-1, -1), -2)
+
+ def test_type(self):
+ self.assertIsInstance(add(1, 2), int)
+
+if __name__ == "__main__":
+ unittest.main()
+```
+
+Run it from the terminal:
+
+```bash title="terminal"
+$ python -m unittest test_math.py
+...
+Ran 3 tests in 0.001s
+OK
+```
+
+### Common assert methods
+
+| Method | Passes when |
+|:---|:---|
+| `assertEqual(a, b)` | `a == b` |
+| `assertNotEqual(a, b)` | `a != b` |
+| `assertTrue(x)` / `assertFalse(x)` | `x` is truthy / falsy |
+| `assertIsNone(x)` | `x is None` |
+| `assertIn(a, b)` | `a in b` |
+| `assertIsInstance(a, cls)` | `a` is an instance of `cls` |
+| `assertRaises(Err)` | The block raises `Err` |
+
+### setUp and tearDown
+
+`setUp` runs before **each** test, `tearDown` after — great for shared fixtures.
+
+```python title="setup_teardown.py"
+import unittest
+
+class TestList(unittest.TestCase):
+ def setUp(self):
+ self.data = [1, 2, 3] # fresh for every test
+
+ def test_append(self):
+ self.data.append(4)
+ self.assertEqual(self.data, [1, 2, 3, 4])
+
+ def test_length(self):
+ self.assertEqual(len(self.data), 3)
+```
+
+### Testing exceptions
+
+```python title="unittest_raises.py"
+import unittest
+
+def divide(a, b):
+ return a / b
+
+class TestDivide(unittest.TestCase):
+ def test_zero(self):
+ with self.assertRaises(ZeroDivisionError):
+ divide(1, 0)
+```
+
+## pytest (third-party)
+
+`pytest` lets you write tests as **plain functions** using ordinary `assert`. Install it first:
+
+```bash title="terminal"
+$ pip install pytest
+```
+
+```python title="test_pytest.py"
+# Functions named test_* are collected automatically.
+def add(a, b):
+ return a + b
+
+def test_positive():
+ assert add(2, 3) == 5
+
+def test_negative():
+ assert add(-1, 1) == 0
+```
+
+```bash title="terminal"
+$ pytest
+==== 2 passed in 0.01s ====
+```
+
+### Parametrize — one test, many cases
+
+```python title="parametrize.py"
+import pytest
+
+def square(n):
+ return n * n
+
+@pytest.mark.parametrize("value,expected", [
+ (2, 4),
+ (3, 9),
+ (-4, 16),
+])
+def test_square(value, expected):
+ assert square(value) == expected
+```
+
+### Fixtures — reusable setup
+
+```python title="fixtures.py"
+import pytest
+
+@pytest.fixture
+def sample_data():
+ return [1, 2, 3]
+
+def test_sum(sample_data): # the fixture is injected by name
+ assert sum(sample_data) == 6
+```
+
+### Testing exceptions in pytest
+
+```python title="pytest_raises.py"
+import pytest
+
+def divide(a, b):
+ return a / b
+
+def test_zero():
+ with pytest.raises(ZeroDivisionError):
+ divide(1, 0)
+```
+
+## unittest vs pytest
+
+| | unittest | pytest |
+|:---|:---|:---|
+| Ships with Python | Yes | No (`pip install pytest`) |
+| Test style | `TestCase` classes | Plain functions |
+| Assertions | `self.assertEqual(...)` | plain `assert` |
+| Fixtures | `setUp`/`tearDown` | `@pytest.fixture` |
+| Parametrize | manual / subTest | `@pytest.mark.parametrize` |
+
+> Both are excellent. `unittest` is always available; `pytest` is more concise and has a rich plugin ecosystem. Many projects write plain `assert` tests and run them with `pytest`.
+
+## Common pitfalls
+
+- **Test functions/methods must start with `test`** or the runner skips them.
+- **Test one thing per test** — small, focused tests pinpoint failures.
+- **Don't depend on test order** — each test should stand alone.
+- **`assertEqual(a, b)` argument order** — convention is `(actual, expected)`.
+
+## Practice Exercises
+
+### Exercise 1 – Assert a function's result
+
+
+
+### Exercise 2 – assertEqual in a TestCase
+
+
+
+### Exercise 3 – Expect an exception
+
+
+
+## Summary
+
+- Tests **arrange, act, assert** to lock in expected behaviour.
+- **`unittest`** (built in) uses `TestCase` classes, `assert*` methods, and `setUp`/`tearDown`.
+- **`pytest`** (install with pip) uses plain functions, bare `assert`, fixtures, and `@parametrize`.
+- Test exceptions with `assertRaises` / `pytest.raises`.
+- Keep tests small, independent, and named `test_*`.
diff --git a/src/content/docs/tutorials/Python Strings/Formatting String.mdx b/src/content/docs/tutorials/Python Strings/Formatting String.mdx
index 0d6a54f4..a582e197 100644
--- a/src/content/docs/tutorials/Python Strings/Formatting String.mdx
+++ b/src/content/docs/tutorials/Python Strings/Formatting String.mdx
@@ -28,6 +28,10 @@ TypeError: can only concatenate str (not "int") to str
The above example will throw an error because we cannot combine strings and numbers like this, using the `+` operator. We can only combine strings with other strings.
:::
+:::tip[Go deeper on f-strings]
+This page introduces all the formatting techniques. For an in-depth look at f-strings — the format spec mini-language (alignment, width, precision, thousands, number bases), the `=` debug specifier, conversions, and nesting — see the [f-strings Deep Dive](../../modern-python/f-strings-deep-dive/).
+:::
+
String formatting is the process of building a string representation dynamically by inserting the value of numeric expressions in an already existing string. Python's string concatenation operator doesn't accept a non-string operand. Hence, Python offers following string formatting techniques −
- The `%` operator
diff --git a/src/content/docs/tutorials/Python Variables/Global Variables.mdx b/src/content/docs/tutorials/Python Variables/Global Variables.mdx
index 82680351..504d5133 100644
--- a/src/content/docs/tutorials/Python Variables/Global Variables.mdx
+++ b/src/content/docs/tutorials/Python Variables/Global Variables.mdx
@@ -7,6 +7,18 @@ sidebar:
We can create a variable outside of a function. This is called a global variable. We can access the global variable inside or outside of a function. Let's see an example.
+A variable's **scope** is the region of the program where it is visible. A variable created outside every function lives in the *global* scope; one created inside a function lives in that function's *local* scope.
+
+**Diagram**:
+```mermaid title="global vs local scope" desc="Where variables are visible"
+graph TD
+ A[Global scope] --> B[name = 'John']
+ A --> C[Function display]
+ C --> D[Local scope: its own names]
+ D -->|can read| B
+ D -->|cannot change unless 'global'| B
+```
+
#### Example:
```python title="variable.py" showLineNumbers{1} {2}
# Global variable
@@ -82,6 +94,21 @@ Hello, Smith
In this example, we used the `global` keyword to access the global variable `name` inside the function `display()`. We can access the global variable `name` inside the function `display()`. We can also access the global variable `name` outside the function `display()`.
+:::caution
+**Use `global` sparingly.** Functions that quietly modify global state are hard to test and debug, because their result depends on data outside the function. Prefer passing values in as parameters and returning results:
+```python title="prefer_return.py" showLineNumbers{1} {1-5}
+# Better than mutating a global
+def increment(value):
+ return value + 1
+
+count = increment(0) # caller controls the state
+```
+:::
+
+:::note
+`global` reaches the module-level scope. To reassign a variable in an *enclosing function* (not the global scope), use the `nonlocal` keyword instead. Both are covered in depth in the [Closures and Scope (LEGB)](/tutorials/closures-and-scope/) tutorial.
+:::
+
---
import DataCampExercise from "../../../../components/DataCampExercise.astro";
diff --git a/src/content/docs/tutorials/Python Variables/Multiple Assignment.mdx b/src/content/docs/tutorials/Python Variables/Multiple Assignment.mdx
index 5871badf..42e8d6b7 100644
--- a/src/content/docs/tutorials/Python Variables/Multiple Assignment.mdx
+++ b/src/content/docs/tutorials/Python Variables/Multiple Assignment.mdx
@@ -98,6 +98,37 @@ C:\Users\Your Name> python variable.py
10 20 30
```
+## Extended Unpacking with `*`
+
+When you do not know (or do not care) how many items are in the middle, a starred variable `*` captures the rest as a list. This is called **extended unpacking**.
+
+```python title="star_unpack.py" showLineNumbers{1} {1-7}
+numbers = [1, 2, 3, 4, 5]
+
+first, *rest = numbers
+print(first, rest) # 1 [2, 3, 4, 5]
+
+first, *middle, last = numbers
+print(first, middle, last) # 1 [2, 3, 4] 5
+```
+
+## Swapping Variables
+
+Multiple assignment gives Python an elegant one-line swap — no temporary variable needed. The right-hand side is built into a tuple first, then unpacked.
+
+```python title="swap.py" showLineNumbers{1} {1-3}
+a, b = 5, 10
+a, b = b, a
+print(a, b) # 10 5
+```
+
+:::caution
+The count must match (unless you use `*`). Too few or too many values raises a `ValueError`:
+```python title="mismatch.py" showLineNumbers{1} {1}
+x, y = 1, 2, 3 # ValueError: too many values to unpack (expected 2)
+```
+:::
+
---
import DataCampExercise from "../../../../components/DataCampExercise.astro";
diff --git a/src/content/docs/tutorials/Python Variables/Naming Convention.mdx b/src/content/docs/tutorials/Python Variables/Naming Convention.mdx
index 2cfeb053..6d9f3ecc 100644
--- a/src/content/docs/tutorials/Python Variables/Naming Convention.mdx
+++ b/src/content/docs/tutorials/Python Variables/Naming Convention.mdx
@@ -57,7 +57,6 @@ my-name = "John"
my name = "John"
```
:::
-B
## Constant Naming Conventions
Constants are usually declared and assigned in a module. Python does not have built-in constant types, but Python programmers use all capital letters to indicate a variable should be treated as a constant and never be changed after it is initialized.
@@ -132,6 +131,44 @@ def set_first_name():
pass
```
+## Which Style Does Python Use?
+
+The three styles above all *run*, but Python's official style guide (**PEP 8**) assigns a specific style to each kind of identifier. Following it makes your code look like everyone else's Python.
+
+| Identifier | Convention | Example |
+| :--- | :--- | :--- |
+| Variable | `snake_case` | `user_age`, `total_price` |
+| Function | `snake_case` | `get_user()`, `calc_total()` |
+| Constant | `UPPER_SNAKE_CASE` | `MAX_SIZE`, `PI` |
+| Class | `PascalCase` (CapWords) | `BankAccount`, `UserProfile` |
+| Module / file | short `lowercase` | `utils.py`, `data_loader.py` |
+
+:::tip
+**`camelCase` is *not* idiomatic Python** even though it works. Use `snake_case` for variables and functions — reserve `PascalCase` for classes.
+:::
+
+## Underscore Conventions
+
+The underscore carries special meaning in several naming patterns:
+
+| Pattern | Meaning |
+| :--- | :--- |
+| `_name` | "Internal use" hint — a weak private signal to other developers. |
+| `name_` | Trailing underscore to avoid clashing with a keyword, e.g. `class_`, `type_`. |
+| `__name` | Name mangling inside a class — Python rewrites it to avoid subclass clashes. |
+| `__name__` | "Dunder" (double underscore) — reserved for Python, e.g. `__init__`, `__name__`. |
+| `_` | A throwaway variable for values you intend to ignore: `for _ in range(3):`. |
+
+```python title="underscores.py" showLineNumbers{1} {1-3}
+_internal = "use within this module"
+class_ = "Biology 101" # avoids the 'class' keyword
+for _ in range(3): # loop counter is unused
+ print("hi")
+```
+
+:::note
+For the full rules, see [PEP 8 — Style Guide for Python Code](https://peps.python.org/pep-0008/).
+:::
---
diff --git a/src/content/docs/tutorials/Python Variables/Variable Assignment.mdx b/src/content/docs/tutorials/Python Variables/Variable Assignment.mdx
index 177cd7b1..1cdb881f 100644
--- a/src/content/docs/tutorials/Python Variables/Variable Assignment.mdx
+++ b/src/content/docs/tutorials/Python Variables/Variable Assignment.mdx
@@ -6,7 +6,16 @@ sidebar:
---
## What is a Variable?
-Python variables are the reserved memory locations used to store values with in a Python Program. This means that when you create a variable you reserve some space in the memory. Based on the data type of a variable, Python interpreter allocates memory and decides what can be stored in the reserved memory. Therefore, by assigning different data types to Python variables, you can store integers, decimals or characters in these variables.
+A variable is a **name** that refers to a value stored in memory. In Python, a variable is best understood as a *label* attached to an object, not a box that holds the value. When you write `x = 10`, Python creates the integer object `10` and binds the name `x` to it. This "name binding" model explains a lot of Python's behaviour.
+
+**Diagram**:
+```mermaid title="variables are names bound to objects" desc="How a variable name references a value in memory"
+graph LR
+ A[Name: x] -->|refers to| B["Object: 10 (int)"]
+ C[Name: y] -->|refers to| B
+```
+
+Therefore, by assigning different data types to Python variables, you can store integers, decimals, strings, or any other object — and the name can be rebound to a different object at any time.
## Declaring Variables in Python
In Python, variables do not need explicit declaration to reserve memory space. The declaration happens automatically when you assign a value to a variable. The equal sign (=) is used to assign values to variables.
@@ -104,6 +113,42 @@ C:\Users\Your Name> python variable.py
20.5
```
+## Reassigning and Dynamic Typing
+
+A variable can be rebound to a value of any type at any time — Python does not lock a name to one type.
+
+```python title="variable.py" showLineNumbers{1} {1-4}
+x = 10 # x refers to an int
+x = "ten" # now x refers to a str — perfectly legal
+x = [1, 2, 3] # now a list
+print(x) # [1, 2, 3]
+```
+
+## Deleting a Variable
+
+The `del` statement removes a name binding. Using the name afterwards raises a `NameError`.
+
+```python title="del.py" showLineNumbers{1} {1-3}
+score = 100
+del score
+# print(score) # NameError: name 'score' is not defined
+```
+
+## Aliasing: Two Names, One Object
+
+Because a variable is just a name, assigning one variable to another makes both names point to the **same** object. For **mutable** objects (like lists) this matters — changing it through one name is visible through the other.
+
+```python title="alias.py" showLineNumbers{1} {1-4}
+a = [1, 2, 3]
+b = a # b and a refer to the SAME list
+b.append(4)
+print(a) # [1, 2, 3, 4] -> changed through b!
+```
+
+:::tip
+To get an independent copy instead of an alias, use `b = a.copy()` (or `list(a)`). See the [Data Types](/tutorials/datatype/) tutorial for mutable vs. immutable.
+:::
+
---
import DataCampExercise from "../../../../components/DataCampExercise.astro";
diff --git a/src/content/docs/tutorials/Syntax.mdx b/src/content/docs/tutorials/Syntax.mdx
index 92b58ccb..219f96b1 100644
--- a/src/content/docs/tutorials/Syntax.mdx
+++ b/src/content/docs/tutorials/Syntax.mdx
@@ -9,6 +9,52 @@ import DataCampExercise from "../../../components/DataCampExercise.astro";
## Python Syntax: Indentation and Its Uniqueness
+Syntax is the set of rules that defines how a Python program is written. Compared to most languages, Python's syntax is famously minimal — no braces, no semicolons at the end of lines, no type declarations. This section covers the rules that matter most when you start.
+
+## Statements and Lines
+
+A **statement** is a single instruction Python executes. By default, one statement sits on one line, and the end of the line ends the statement — no semicolon needed.
+
+```python title="statements.py" showLineNumbers{1}
+x = 5
+y = 10
+print(x + y)
+```
+
+You *can* place multiple statements on one line with a semicolon, though it is discouraged for readability:
+
+```python title="oneline.py" showLineNumbers{1}
+x = 5; y = 10; print(x + y) # works, but avoid this style
+```
+
+### Line Continuation
+
+For a long statement, break it across lines using a backslash `\`, or — preferably — wrap it in parentheses `()`, brackets `[]`, or braces `{}`, inside which Python ignores line breaks:
+
+```python title="continuation.py" showLineNumbers{1} {2-3,6-10}
+# Explicit continuation with a backslash
+total = 1 + 2 + 3 + \
+ 4 + 5 + 6
+
+# Implicit continuation inside brackets (preferred)
+numbers = [
+ 1, 2, 3,
+ 4, 5, 6,
+]
+```
+
+## Code Blocks and the Colon
+
+A **block** is a group of statements that belong together (the body of an `if`, a loop, a function). In Python, a block is introduced by a colon `:` on the previous line and defined by the indentation that follows.
+
+**Diagram**:
+```mermaid title="Python block structure" desc="How a colon and indentation define a block"
+graph TD
+ A["Header line ends with ':'"] --> B[Indented body = the block]
+ B --> C[Dedent = block ends]
+ C --> D[Back to outer level]
+```
+
## Indentation in Python
Unlike many other programming languages that use braces `{}` or keywords like `begin` and `end` to define blocks of code, Python relies on indentation to signify the beginning and end of blocks.
@@ -27,15 +73,20 @@ else:
In the above example, the lines of code within the `if` and `else` blocks are indented to indicate the scope of each block. The level of indentation is crucial, as it determines the structure of the code.
:::danger
-You must use the same number of spaces or tabs for each level of indentation. Mixing tabs and spaces can lead to errors.
-```python title="indent.py" showLineNumbers{1}
+You must use the same number of spaces or tabs for each level of indentation. Missing or inconsistent indentation raises an error. The code below is **broken** — the body of `if` is not indented:
+```python title="broken.py" showLineNumbers{1}
if True:
-print("This is indented.")
- print("So is this.")
-else:
-print("This is not indented.")
+print("This is indented.") # ERROR: expected an indented block
```
-
+Running it produces:
+```cmd title="command" showLineNumbers{1} {2-4}
+C:\Users\Your Name> python broken.py
+ File "broken.py", line 2
+ print("This is indented.")
+ ^
+IndentationError: expected an indented block after 'if' statement on line 1
+```
+Mixing tabs and spaces in the same block raises a `TabError`. Pick one (spaces are the convention) and stay consistent.
:::
## Importance of Indentation
@@ -66,6 +117,33 @@ In this C++ example, blocks are defined by braces `{}`. The indentation is not s
3. **Use an Editor with Indentation Support:** Many code editors automatically handle indentation for you. Configure your editor to use spaces or tabs consistently.
+## Python is Case-Sensitive
+
+Python treats uppercase and lowercase letters as different. `Name`, `name`, and `NAME` are three separate identifiers, and keywords must be lowercase (`True`, not `true` — except the booleans which are capitalised).
+
+```python title="case.py" showLineNumbers{1}
+name = "Alice"
+Name = "Bob"
+print(name) # Alice
+print(Name) # Bob
+```
+
+## Keywords and Identifiers
+
+**Keywords** are reserved words with special meaning (`if`, `for`, `def`, `return`, `class`, ...). You cannot use them as variable names. **Identifiers** are the names you give to variables, functions, and classes; they may contain letters, digits, and underscores, but cannot start with a digit.
+
+```python title="keywords.py" showLineNumbers{1}
+import keyword
+print(keyword.kwlist) # prints every reserved keyword
+```
+
+Output:
+
+```cmd title="command" showLineNumbers{1} {2}
+C:\Users\Your Name> python keywords.py
+['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', ...]
+```
+
## Conclusion
Understanding Python's indentation syntax is crucial for writing clean and readable code. Embrace the uniqueness of Python's syntax, and you'll find that it contributes to the language's elegance and readability.
diff --git a/src/pages/rss.xml.js b/src/pages/rss.xml.js
new file mode 100644
index 00000000..aa42a337
--- /dev/null
+++ b/src/pages/rss.xml.js
@@ -0,0 +1,27 @@
+import rss from "@astrojs/rss";
+import { getCollection } from "astro:content";
+
+export async function GET(context) {
+ const docs = await getCollection("docs");
+
+ // Syndicate tutorials and project pages (skip drafts and the 404 page).
+ const items = docs
+ .filter((entry) => {
+ const id = entry.id.toLowerCase();
+ return id.startsWith("tutorials/") || id.startsWith("projects/");
+ })
+ .filter((entry) => entry.slug !== "404")
+ .map((entry) => ({
+ title: entry.data.title,
+ description: entry.data.description ?? "",
+ link: `/${entry.slug}/`,
+ }));
+
+ return rss({
+ title: "Python Central Hub",
+ description:
+ "Tutorials and hands-on Python projects from Python Central Hub.",
+ site: context.site,
+ items,
+ });
+}
diff --git a/src/styles/global.css b/src/styles/global.css
index 26c6b305..7017fd12 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -2,6 +2,27 @@
@tailwind components;
@tailwind utilities;
+/* Skip-to-content link: hidden until focused, then visible top-left. */
+.skip-to-content {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1000;
+ padding: 0.5rem 1rem;
+ background: var(--sl-color-accent-high, #e7c384);
+ color: var(--sl-color-black, #000);
+ border-radius: 0 0 0.5rem 0;
+ font-weight: 600;
+ transform: translateY(-150%);
+ transition: transform 0.2s ease-in-out;
+}
+.skip-to-content:focus,
+.skip-to-content:focus-visible {
+ transform: translateY(0);
+ outline: 2px solid var(--sl-color-accent-high);
+ outline-offset: 2px;
+}
+
/* * {
margin: 0;
padding: 0;