From 73cfc25916dc883deb0bf4f2d3f34e13ad11bbb4 Mon Sep 17 00:00:00 2001 From: Reza Hassani Date: Tue, 23 Jun 2026 16:20:17 +0330 Subject: [PATCH 1/6] Create FUNDING.yml --- .github/FUNDING.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c69cee8 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,6 @@ +# GitHub Sponsorship Configuration +# Place this file at: .github/FUNDING.yml + +patreon: hassanireza +ko_fi: hassanireza +buy_me_a_coffee: hassanireza From 5f3748d27da6a036911460a87567b2afebc8b4e3 Mon Sep 17 00:00:00 2001 From: Reza Hassani Date: Mon, 6 Jul 2026 14:10:15 +0330 Subject: [PATCH 2/6] Delete .github/FUNDING.yml --- .github/FUNDING.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index c69cee8..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,6 +0,0 @@ -# GitHub Sponsorship Configuration -# Place this file at: .github/FUNDING.yml - -patreon: hassanireza -ko_fi: hassanireza -buy_me_a_coffee: hassanireza From 799979775c2a7e924237561df755b8f170927c25 Mon Sep 17 00:00:00 2001 From: Reza Hassani Date: Mon, 6 Jul 2026 14:11:25 +0330 Subject: [PATCH 3/6] Add files via upload --- LICENSE | 2 +- README.md | 454 ++++++++++++++++++++------------ docs/architecture.svg | 58 ++++ docs/banner.svg | 52 ++++ index.html | 15 ++ package.json | 35 +++ public/banner.svg | 52 ++++ public/favicon.svg | 14 + src/App.tsx | 23 ++ src/components/Certificate.tsx | 81 ++++++ src/components/CodeBlock.tsx | 34 +++ src/components/Footer.css | 20 ++ src/components/Footer.tsx | 18 ++ src/components/Layout.css | 30 +++ src/components/Layout.tsx | 24 ++ src/components/Navbar.css | 163 ++++++++++++ src/components/Navbar.tsx | 67 +++++ src/components/Sidebar.css | 111 ++++++++ src/components/Sidebar.tsx | 49 ++++ src/content/day-01/lesson.md | 134 ++++++++++ src/content/day-01/solution.py | 27 ++ src/content/day-02/lesson.md | 170 ++++++++++++ src/content/day-02/solution.py | 39 +++ src/content/day-03/lesson.md | 185 +++++++++++++ src/content/day-03/solution.py | 49 ++++ src/content/day-04/lesson.md | 199 ++++++++++++++ src/content/day-04/solution.py | 91 +++++++ src/content/day-05/lesson.md | 165 ++++++++++++ src/content/day-05/solution.py | 82 ++++++ src/content/day-06/lesson.md | 175 ++++++++++++ src/content/day-06/solution.py | 64 +++++ src/content/day-07/lesson.md | 181 +++++++++++++ src/content/day-07/solution.py | 66 +++++ src/content/day-08/lesson.md | 187 +++++++++++++ src/content/day-08/solution.py | 70 +++++ src/content/day-09/lesson.md | 157 +++++++++++ src/content/day-09/solution.py | 55 ++++ src/content/day-10/lesson.md | 169 ++++++++++++ src/content/day-10/solution.py | 64 +++++ src/content/day-11/lesson.md | 187 +++++++++++++ src/content/day-11/solution.py | 84 ++++++ src/content/day-12/lesson.md | 201 ++++++++++++++ src/content/day-12/solution.py | 102 +++++++ src/content/day-13/lesson.md | 182 +++++++++++++ src/content/day-13/solution.py | 83 ++++++ src/content/day-14/lesson.md | 177 +++++++++++++ src/content/day-14/solution.py | 74 ++++++ src/content/day-15/lesson.md | 191 ++++++++++++++ src/content/day-15/solution.py | 91 +++++++ src/content/day-16/lesson.md | 178 +++++++++++++ src/content/day-16/solution.py | 83 ++++++ src/content/day-17/lesson.md | 165 ++++++++++++ src/content/day-17/solution.py | 77 ++++++ src/content/day-18/lesson.md | 149 +++++++++++ src/content/day-18/solution.py | 57 ++++ src/content/day-19/lesson.md | 191 ++++++++++++++ src/content/day-19/solution.py | 89 +++++++ src/content/day-20/lesson.md | 192 ++++++++++++++ src/content/day-20/solution.py | 108 ++++++++ src/content/day-21/lesson.md | 228 ++++++++++++++++ src/content/day-21/solution.py | 115 ++++++++ src/content/day-22/lesson.md | 197 ++++++++++++++ src/content/day-22/solution.py | 111 ++++++++ src/content/day-23/lesson.md | 215 +++++++++++++++ src/content/day-23/solution.py | 120 +++++++++ src/content/day-24/lesson.md | 183 +++++++++++++ src/content/day-24/solution.py | 89 +++++++ src/content/day-25/lesson.md | 198 ++++++++++++++ src/content/day-25/solution.py | 98 +++++++ src/content/day-26/lesson.md | 163 ++++++++++++ src/content/day-26/solution.py | 75 ++++++ src/content/day-27/lesson.md | 180 +++++++++++++ src/content/day-27/solution.py | 81 ++++++ src/content/day-28/lesson.md | 171 ++++++++++++ src/content/day-28/solution.py | 87 ++++++ src/content/day-29/lesson.md | 275 +++++++++++++++++++ src/content/day-29/solution.py | 175 ++++++++++++ src/content/day-30/lesson.md | 373 ++++++++++++++++++++++++++ src/content/day-30/solution.py | 285 ++++++++++++++++++++ src/context/AuthContext.tsx | 102 +++++++ src/context/ProgressContext.tsx | 66 +++++ src/data/content.ts | 29 ++ src/data/curriculum.ts | 58 ++++ src/index.css | 169 ++++++++++++ src/main.tsx | 19 ++ src/pages/Auth.css | 84 ++++++ src/pages/CertificatePage.css | 79 ++++++ src/pages/CertificatePage.tsx | 89 +++++++ src/pages/DayPage.css | 184 +++++++++++++ src/pages/DayPage.tsx | 103 ++++++++ src/pages/Home.css | 164 ++++++++++++ src/pages/Home.tsx | 85 ++++++ src/pages/Login.tsx | 72 +++++ src/pages/NotFound.tsx | 15 ++ src/pages/Signup.tsx | 84 ++++++ src/vite-env.d.ts | 1 + tsconfig.json | 23 ++ tsconfig.node.json | 10 + vite.config.ts | 16 ++ 99 files changed, 10999 insertions(+), 164 deletions(-) create mode 100644 docs/architecture.svg create mode 100644 docs/banner.svg create mode 100644 index.html create mode 100644 package.json create mode 100644 public/banner.svg create mode 100644 public/favicon.svg create mode 100644 src/App.tsx create mode 100644 src/components/Certificate.tsx create mode 100644 src/components/CodeBlock.tsx create mode 100644 src/components/Footer.css create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Layout.css create mode 100644 src/components/Layout.tsx create mode 100644 src/components/Navbar.css create mode 100644 src/components/Navbar.tsx create mode 100644 src/components/Sidebar.css create mode 100644 src/components/Sidebar.tsx create mode 100644 src/content/day-01/lesson.md create mode 100644 src/content/day-01/solution.py create mode 100644 src/content/day-02/lesson.md create mode 100644 src/content/day-02/solution.py create mode 100644 src/content/day-03/lesson.md create mode 100644 src/content/day-03/solution.py create mode 100644 src/content/day-04/lesson.md create mode 100644 src/content/day-04/solution.py create mode 100644 src/content/day-05/lesson.md create mode 100644 src/content/day-05/solution.py create mode 100644 src/content/day-06/lesson.md create mode 100644 src/content/day-06/solution.py create mode 100644 src/content/day-07/lesson.md create mode 100644 src/content/day-07/solution.py create mode 100644 src/content/day-08/lesson.md create mode 100644 src/content/day-08/solution.py create mode 100644 src/content/day-09/lesson.md create mode 100644 src/content/day-09/solution.py create mode 100644 src/content/day-10/lesson.md create mode 100644 src/content/day-10/solution.py create mode 100644 src/content/day-11/lesson.md create mode 100644 src/content/day-11/solution.py create mode 100644 src/content/day-12/lesson.md create mode 100644 src/content/day-12/solution.py create mode 100644 src/content/day-13/lesson.md create mode 100644 src/content/day-13/solution.py create mode 100644 src/content/day-14/lesson.md create mode 100644 src/content/day-14/solution.py create mode 100644 src/content/day-15/lesson.md create mode 100644 src/content/day-15/solution.py create mode 100644 src/content/day-16/lesson.md create mode 100644 src/content/day-16/solution.py create mode 100644 src/content/day-17/lesson.md create mode 100644 src/content/day-17/solution.py create mode 100644 src/content/day-18/lesson.md create mode 100644 src/content/day-18/solution.py create mode 100644 src/content/day-19/lesson.md create mode 100644 src/content/day-19/solution.py create mode 100644 src/content/day-20/lesson.md create mode 100644 src/content/day-20/solution.py create mode 100644 src/content/day-21/lesson.md create mode 100644 src/content/day-21/solution.py create mode 100644 src/content/day-22/lesson.md create mode 100644 src/content/day-22/solution.py create mode 100644 src/content/day-23/lesson.md create mode 100644 src/content/day-23/solution.py create mode 100644 src/content/day-24/lesson.md create mode 100644 src/content/day-24/solution.py create mode 100644 src/content/day-25/lesson.md create mode 100644 src/content/day-25/solution.py create mode 100644 src/content/day-26/lesson.md create mode 100644 src/content/day-26/solution.py create mode 100644 src/content/day-27/lesson.md create mode 100644 src/content/day-27/solution.py create mode 100644 src/content/day-28/lesson.md create mode 100644 src/content/day-28/solution.py create mode 100644 src/content/day-29/lesson.md create mode 100644 src/content/day-29/solution.py create mode 100644 src/content/day-30/lesson.md create mode 100644 src/content/day-30/solution.py create mode 100644 src/context/AuthContext.tsx create mode 100644 src/context/ProgressContext.tsx create mode 100644 src/data/content.ts create mode 100644 src/data/curriculum.ts create mode 100644 src/index.css create mode 100644 src/main.tsx create mode 100644 src/pages/Auth.css create mode 100644 src/pages/CertificatePage.css create mode 100644 src/pages/CertificatePage.tsx create mode 100644 src/pages/DayPage.css create mode 100644 src/pages/DayPage.tsx create mode 100644 src/pages/Home.css create mode 100644 src/pages/Home.tsx create mode 100644 src/pages/Login.tsx create mode 100644 src/pages/NotFound.tsx create mode 100644 src/pages/Signup.tsx create mode 100644 src/vite-env.d.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/LICENSE b/LICENSE index cdd41ec..c375363 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Learn JavaScript in 30 Days Contributors +Copyright (c) 2026 Python in 30 Days Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 1f64d17..26a8032 100644 --- a/README.md +++ b/README.md @@ -1,240 +1,368 @@
-Learn JavaScript in 30 Days - a free 30 day JavaScript course with 30 projects +Python in 30 Days -# Learn JavaScript in 30 Days +
-### A free, open source, project based course that teaches you JavaScript from scratch with 30 lessons and 30 hands-on projects +[![React](https://img.shields.io/badge/React-18.3-149ECA?style=for-the-badge&logo=react&logoColor=white)](https://react.dev) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.5-3178C6?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org) +[![Vite](https://img.shields.io/badge/Vite-5.4-646CFF?style=for-the-badge&logo=vite&logoColor=white)](https://vitejs.dev) +[![License](https://img.shields.io/badge/License-MIT-FFD43B?style=for-the-badge)](./LICENSE) +[![Deploy](https://img.shields.io/badge/Deploy-GitHub%20Pages-222222?style=for-the-badge&logo=github)](https://pages.github.com) -[![Stars](https://img.shields.io/github/stars/hassanireza/learn-javascript-30-days?style=for-the-badge&color=f7df1e&labelColor=0d1117)](https://github.com/your-username/learn-javascript-30-days/stargazers) -[![Forks](https://img.shields.io/github/forks/hassanireza/learn-javascript-30-days?style=for-the-badge&color=58a6ff&labelColor=0d1117)](https://github.com/your-username/learn-javascript-30-days/network/members) -[![License](https://img.shields.io/github/license/hassanireza/learn-javascript-30-days?style=for-the-badge&color=238636&labelColor=0d1117)](./LICENSE) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-db61a2?style=for-the-badge&labelColor=0d1117)](./CONTRIBUTING.md) -[![Made with JavaScript](https://img.shields.io/badge/Made%20with-JavaScript-f7df1e?style=for-the-badge&labelColor=0d1117)](https://developer.mozilla.org/en-US/docs/Web/JavaScript) +
-**[Start Learning](./days/day-01-introduction-and-setup/README.md)**  ·  **[View Curriculum](#curriculum)**  ·  **[View Projects](#projects)**  ·  **[Contribute](./CONTRIBUTING.md)** +> **A free, interactive Python programming course rebuilt as a modern single page application. Thirty daily lessons, thirty hands on projects, full navigation freedom, and a downloadable certificate. No login required to learn, no login required to graduate.** + +
+ +[![Live Demo](https://img.shields.io/badge/%E2%96%B6%20LIVE%20DEMO-Visit%20Site-FFD43B?style=for-the-badge)](#) +   +[![Get Started](https://img.shields.io/badge/%F0%9F%9A%80%20GET%20STARTED-Local%20Setup-3776AB?style=for-the-badge)](#getting-started)
--- -## About This Course +## Table of Contents + +- [About](#about) +- [Key Principles](#key-principles) +- [Feature Overview](#feature-overview) +- [Screenshots](#screenshots) +- [Tech Stack](#tech-stack) +- [Architecture](#architecture) +- [Project Structure](#project-structure) +- [Getting Started](#getting-started) +- [Available Scripts](#available-scripts) +- [Deployment to GitHub Pages](#deployment-to-github-pages) +- [Curriculum](#curriculum) +- [Authentication Model](#authentication-model) +- [Certificate](#certificate) +- [Accessibility and Design](#accessibility-and-design) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [License](#license) + +--- + +## About + +This repository is a TypeScript and React rebuild of the original **Python in 30 Days** Markdown course. Every lesson and every project solution from the original curriculum has been preserved, and wrapped in a fast, accessible, single page application that works entirely in the browser with zero backend. -**Learn JavaScript in 30 Days** is a structured, beginner friendly JavaScript course built entirely with Markdown and hosted on GitHub. It is designed for people who have never written a line of JavaScript before and want a clear, daily path to becoming job ready in modern JavaScript. +The rebuild was guided by one rule above all others: **learning should never be blocked**. There is no wall between a visitor and the content. There is no test a learner must pass to move forward. There is no account required to read a single lesson or to walk away with a certificate. -Each day introduces one focused topic, explains it with correct and tested code examples, and ends with a small project you build yourself. By Day 30 you will have written 30 real projects and understand JavaScript fundamentals, the DOM, asynchronous programming, and modern ES6+ syntax well enough to start building real applications or move on to frameworks such as React, Vue, or Node.js. +## Key Principles -This repository is intentionally **100% Markdown and plain JavaScript**. No build tools, no paid content, no video paywalls. Open a lesson, read it, write the code, and move to the next one. +| Principle | What it means in this app | +|---|---| +| Content first | Every one of the 30 lessons and 30 project solutions is reachable in one click from the sidebar, with no gating. | +| Free navigation | Previous and Next buttons always work. Nothing checks whether you "passed" the current day before letting you continue. | +| Optional accounts | A login and signup flow exists purely as a UI showcase. It personalizes a greeting and nothing more. | +| No login certificate | The certificate page asks for a name, renders a certificate live, and lets you download it as a PNG. No account, no server, no quiz score required. | +| Static and portable | The whole app is a static bundle. It ships to GitHub Pages with a single GitHub Actions workflow and needs no server, database or API keys. | -### Why this course is different +## Feature Overview -| | This Course | Typical Tutorials | +- **30 day curriculum** covering fundamentals, data structures, control flow, functions, error handling, file I/O, Pythonic idioms, object oriented programming, decorators, generators, context managers, regular expressions, JSON and CSV, working with APIs, and testing with pytest. +- **Markdown powered lessons** rendered with syntax highlighted code blocks, tables, and callouts, sourced from the original course content. +- **Project solution viewer** displaying the day's full `solution.py` file with Python syntax highlighting. +- **Free form navigation** with a persistent sidebar grouped by topic, a home page grid of every day, and Previous and Next controls on each lesson. +- **Optional progress tracker** stored in the browser that lets a learner mark days complete, purely for their own bookkeeping. It is never used to lock content. +- **Showcase authentication** with login and signup screens, clearly labeled as a demonstration, backed by `localStorage` instead of a real backend. +- **No login certificate generator** that renders an SVG certificate live and exports it as a downloadable PNG image. +- **Responsive, accessible UI** that works on mobile, tablet and desktop, with a collapsible navigation drawer on small screens. +- **Continuous deployment** to GitHub Pages through a GitHub Actions workflow that lints, type checks, builds and publishes the site automatically on every push to `main`. + +## Screenshots + +
+ +| Home | Lesson View | Certificate | |---|---|---| -| Format | Structured Markdown, version controlled on GitHub | Scattered videos and blog posts | -| Practice | A real project every single day | Mostly theory, few projects | -| Navigation | Linked previous/next lessons, no rabbit holes | Hard to follow a clear order | -| Cost | Free and open source forever | Often paywalled | -| Best Practices | Based on MDN, ECMAScript specification, and Airbnb/Google style guides | Inconsistent or outdated advice | -| Depth | Fundamentals to async JavaScript and OOP | Often stops at the basics | +| Curriculum grid with progress stats | Markdown lesson with code and project panel | Live certificate preview and PNG export | ---- +
-## Who This Course Is For +> Replace the placeholders above with real screenshots once the project is deployed. Recommended path: `docs/screenshots/home.png`, `docs/screenshots/lesson.png`, `docs/screenshots/certificate.png`. -- Complete beginners who have never coded before -- Developers from another language who want to learn JavaScript properly -- Students preparing for technical interviews who need a structured refresher -- Bootcamp graduates who want extra project based practice -- Anyone who wants a free alternative to paid JavaScript courses +## Tech Stack -## Prerequisites +
-You do not need any prior programming experience. You only need: +[![React](https://img.shields.io/badge/React-18-149ECA?logo=react&logoColor=white)](https://react.dev) +[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org) +[![Vite](https://img.shields.io/badge/Vite-5-646CFF?logo=vite&logoColor=white)](https://vitejs.dev) +[![React Router](https://img.shields.io/badge/React%20Router-6-CA4245?logo=reactrouter&logoColor=white)](https://reactrouter.com) +[![ESLint](https://img.shields.io/badge/ESLint-8-4B32C3?logo=eslint&logoColor=white)](https://eslint.org) +[![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-CI%2FCD-2088FF?logo=githubactions&logoColor=white)](https://github.com/features/actions) +[![GitHub Pages](https://img.shields.io/badge/GitHub%20Pages-Hosting-222222?logo=github&logoColor=white)](https://pages.github.com) -- A computer with **Windows, macOS, or Linux** -- A modern web browser, such as **Google Chrome** or **Mozilla Firefox** -- A free code editor, such as **[Visual Studio Code](https://code.visualstudio.com/)** -- Curiosity and roughly **30 to 60 minutes a day** +
---- +| Layer | Choice | Why | +|---|---|---| +| UI framework | React 18 with function components and hooks | Predictable state management for navigation, auth and progress | +| Language | TypeScript, strict mode | Type safety across routing, context and content loading | +| Build tool | Vite | Instant dev server, native ES modules, first class static asset handling | +| Routing | React Router 6, `HashRouter` | Client side routing that survives a full page refresh on GitHub Pages without a server rewrite rule | +| Content | Raw Markdown and Python files bundled with `import.meta.glob` | Keeps the original lesson content as the single source of truth, no CMS or database needed | +| Markdown rendering | `react-markdown` with `remark-gfm` | Tables, task lists and full GitHub flavored Markdown support | +| Code highlighting | `react-syntax-highlighter` | Accurate Python syntax highlighting for lessons and project solutions | +| State | React Context plus `localStorage` | Lightweight, dependency free persistence for the showcase auth and progress tracker | +| CI/CD | GitHub Actions | Lint, type check, build and deploy automatically on every push | +| Hosting | GitHub Pages | Free static hosting with a custom domain option | + +## Architecture -## How To Use This Repository +
+Architecture diagram +
-This course follows a simple, predictable folder structure so you always know where you are and what to do next. +The application is intentionally simple: -```text -learn-javascript-30-days/ -├── README.md <- you are here -├── GETTING_STARTED.md <- environment setup guide -├── CONTRIBUTING.md -├── LICENSE -├── assets/ -│ └── banner.svg -├── days/ -│ ├── day-01-introduction-and-setup/ -│ │ └── README.md <- lesson 1 -│ ├── day-02-variables-and-data-types/ -│ │ └── README.md <- lesson 2 -│ └── ... <- through day-30 -└── projects/ - ├── day-01-hello-console/ - │ ├── README.md <- project brief - │ ├── index.html - │ ├── style.css - │ └── script.js - └── ... <- through day-30 -``` +1. Thirty lesson files (`lesson.md`) and thirty project files (`solution.py`) live under `src/content/day-XX/`. +2. Vite bundles that content at build time as raw text using `import.meta.glob`, so there is no runtime fetch and no flash of missing content. +3. React Router renders a `Layout` shell (navbar plus sidebar) around each page. The `DayPage` route resolves a day number from the URL, pulls the matching Markdown and Python content, and renders it with free Previous and Next controls. +4. Two React contexts back the interactive pieces of the UI: `AuthContext` for the showcase login state and `ProgressContext` for the optional "mark complete" tracker. Both persist to `localStorage` only, keeping the app entirely static. +5. GitHub Actions builds the app on every push to `main`, then publishes the `dist/` output to GitHub Pages using the official `actions/deploy-pages` action. -### Step by step +## Project Structure -1. **Fork or clone this repository.** +```text +python-30-days-react/ +├── .github/ +│ └── workflows/ +│ ├── ci.yml CI checks on pull requests +│ └── deploy.yml Build and deploy to GitHub Pages +├── docs/ +│ ├── banner.svg +│ └── architecture.svg +├── public/ +│ └── favicon.svg +├── src/ +│ ├── components/ +│ │ ├── Layout.tsx Page shell: navbar + sidebar + outlet +│ │ ├── Navbar.tsx Top navigation, progress meter, auth controls +│ │ ├── Sidebar.tsx Full curriculum list grouped by topic +│ │ ├── Footer.tsx +│ │ ├── CodeBlock.tsx Syntax highlighted code renderer +│ │ └── Certificate.tsx SVG certificate template +│ ├── context/ +│ │ ├── AuthContext.tsx Showcase-only auth, localStorage backed +│ │ └── ProgressContext.tsx Optional, non-blocking progress tracker +│ ├── data/ +│ │ ├── curriculum.ts Static metadata for all 30 days +│ │ └── content.ts Loads raw lesson/solution files at build time +│ ├── content/ +│ │ └── day-01 .. day-30/ +│ │ ├── lesson.md +│ │ └── solution.py +│ ├── pages/ +│ │ ├── Home.tsx +│ │ ├── DayPage.tsx +│ │ ├── Login.tsx +│ │ ├── Signup.tsx +│ │ ├── CertificatePage.tsx +│ │ └── NotFound.tsx +│ ├── App.tsx +│ ├── main.tsx +│ └── index.css +├── index.html +├── vite.config.ts +├── tsconfig.json +├── package.json +└── README.md +``` - ```bash - git clone https://github.com/your-username/learn-javascript-30-days.git - cd learn-javascript-30-days - ``` +## Getting Started -2. **Open the folder in Visual Studio Code.** +### Prerequisites - ```bash - code . - ``` +- [Node.js](https://nodejs.org) 20 or later +- npm 10 or later (bundled with Node.js) -3. **Start with Day 1.** Open [`days/day-01-introduction-and-setup/README.md`](./days/day-01-introduction-and-setup/README.md) and read the lesson. +### Installation -4. **Build the project for that day** inside the matching folder in `projects/`. Each project folder already contains starter files (`index.html`, `style.css`, `script.js`) with comments telling you exactly what to build. +```bash +git clone https://github.com//python-30-days-react.git +cd python-30-days-react +npm install +``` -5. **Use the navigation links** at the bottom of every lesson to move to the next day, or back to the previous one. There is no need to come back to this README until you finish the course. +### Run the development server -6. **Commit your progress** as you go, so you build a public history of your learning: +```bash +npm run dev +``` - ```bash - git add . - git commit -m "Completed Day 1: Introduction and setup" - git push - ``` +The app will be available at `http://localhost:5173`. -For detailed environment setup, including installing Node.js and a Live Server extension, see **[GETTING_STARTED.md](./GETTING_STARTED.md)**. +### Build for production ---- +```bash +npm run build +npm run preview +``` -## Curriculum +## Available Scripts -The course is split into five stages of six days each, moving from core fundamentals to real asynchronous, object oriented JavaScript. +| Script | Description | +|---|---| +| `npm run dev` | Starts the Vite development server with hot module reload | +| `npm run build` | Type checks the project and produces an optimized build in `dist/` | +| `npm run preview` | Serves the production build locally to sanity check before deploying | +| `npm run lint` | Runs ESLint across the codebase | -### Stage 1 - JavaScript Fundamentals (Days 1-6) +## Deployment to GitHub Pages -| Day | Lesson | Project | -|---|---|---| -| 01 | [Introduction and Setup](./days/day-01-introduction-and-setup/README.md) | [Hello Console](./projects/day-01-hello-console/README.md) | -| 02 | [Variables and Data Types](./days/day-02-variables-and-data-types/README.md) | [Personal Bio Card](./projects/day-02-personal-bio-card/README.md) | -| 03 | [Operators and Expressions](./days/day-03-operators-and-expressions/README.md) | [Simple Calculator](./projects/day-03-simple-calculator/README.md) | -| 04 | [Conditional Statements](./days/day-04-conditional-statements/README.md) | [Grade Calculator](./projects/day-04-grade-calculator/README.md) | -| 05 | [Loops and Iteration](./days/day-05-loops-and-iteration/README.md) | [Multiplication Table Generator](./projects/day-05-multiplication-table-generator/README.md) | -| 06 | [Functions](./days/day-06-functions/README.md) | [Tip Calculator](./projects/day-06-tip-calculator/README.md) | +This project deploys itself. The included workflow at `.github/workflows/deploy.yml` runs on every push to `main` and: -### Stage 2 - Data Structures (Days 7-11) +1. Checks out the repository +2. Installs dependencies with `npm ci` +3. Lints and type checks the project +4. Builds the production bundle with `npm run build` +5. Uploads and publishes the `dist/` folder to GitHub Pages using `actions/deploy-pages` -| Day | Lesson | Project | -|---|---|---| -| 07 | [Arrays](./days/day-07-arrays/README.md) | [Console Todo List](./projects/day-07-console-todo-list/README.md) | -| 08 | [Array Methods](./days/day-08-array-methods/README.md) | [Shopping Cart Total](./projects/day-08-shopping-cart-total/README.md) | -| 09 | [Objects](./days/day-09-objects/README.md) | [Contact Book](./projects/day-09-contact-book/README.md) | -| 10 | [Object Methods and this](./days/day-10-object-methods-and-this/README.md) | [Bank Account Simulator](./projects/day-10-bank-account-simulator/README.md) | -| 11 | [Strings in Depth](./days/day-11-strings-in-depth/README.md) | [Palindrome Checker](./projects/day-11-palindrome-checker/README.md) | +### One time repository setup -### Stage 3 - The Browser and the DOM (Days 12-15) +1. Push this repository to GitHub. +2. In your repository, open **Settings → Pages**. +3. Under **Build and deployment**, set **Source** to **GitHub Actions**. +4. If your repository name is not `python-30-days-react`, update the `REPO_NAME` constant in `vite.config.ts` so the built asset paths match your GitHub Pages sub path. +5. Push to `main`. The **Deploy to GitHub Pages** workflow will build and publish automatically. Your site will be live at: -| Day | Lesson | Project | -|---|---|---| -| 12 | [Introduction to the DOM](./days/day-12-introduction-to-the-dom/README.md) | [DOM Playground](./projects/day-12-dom-playground/README.md) | -| 13 | [DOM Events](./days/day-13-dom-events/README.md) | [Color Switcher](./projects/day-13-color-switcher/README.md) | -| 14 | [Forms and Validation](./days/day-14-forms-and-validation/README.md) | [Sign Up Form Validator](./projects/day-14-sign-up-form-validator/README.md) | -| 15 | [Building a To-Do App](./days/day-15-building-a-todo-app/README.md) | [Interactive To-Do List](./projects/day-15-interactive-todo-list/README.md) | +```text +https://.github.io// +``` -### Stage 4 - Modern JavaScript (Days 16-21) +The app uses React Router's `HashRouter`, so deep links such as `/#/day/12` work correctly on GitHub Pages without needing a custom 404 redirect trick. -| Day | Lesson | Project | -|---|---|---| -| 16 | [Scope and Closures](./days/day-16-scope-and-closures/README.md) | [Counter Factory](./projects/day-16-counter-factory/README.md) | -| 17 | [ES6+ Syntax](./days/day-17-es6-plus-syntax/README.md) | [Recipe Card Renderer](./projects/day-17-recipe-card-renderer/README.md) | -| 18 | [Spread, Rest, and Default Parameters](./days/day-18-spread-rest-and-default-parameters/README.md) | [Mini Utility Library](./projects/day-18-mini-utility-library/README.md) | -| 19 | [Higher Order Functions](./days/day-19-higher-order-functions/README.md) | [Mini Sales Dashboard](./projects/day-19-mini-sales-dashboard/README.md) | -| 20 | [Error Handling](./days/day-20-error-handling/README.md) | [Safe Calculator](./projects/day-20-safe-calculator/README.md) | -| 21 | [Asynchronous JavaScript Basics](./days/day-21-asynchronous-javascript-basics/README.md) | [Simulated Loading Sequence](./projects/day-21-simulated-loading-sequence/README.md) | +## Curriculum -### Stage 5 - Async, OOP, and the Real World (Days 22-30) +
-| Day | Lesson | Project | +| Week | Focus | Days | |---|---|---| -| 22 | [Promises](./days/day-22-promises/README.md) | [Random User Card Generator](./projects/day-22-random-user-card-generator/README.md) | -| 23 | [Async and Await](./days/day-23-async-and-await/README.md) | [Quote Generator](./projects/day-23-quote-generator/README.md) | -| 24 | [Fetch API and JSON](./days/day-24-fetch-api-and-json/README.md) | [GitHub Profile Finder](./projects/day-24-github-profile-finder/README.md) | -| 25 | [Local Storage](./days/day-25-local-storage/README.md) | [Persistent To-Do List](./projects/day-25-persistent-todo-list/README.md) | -| 26 | [Classes and OOP](./days/day-26-classes-and-oop/README.md) | [Library Management System](./projects/day-26-library-management-system/README.md) | -| 27 | [JavaScript Modules](./days/day-27-javascript-modules/README.md) | [Modular Calculator](./projects/day-27-modular-calculator/README.md) | -| 28 | [Regular Expressions](./days/day-28-regular-expressions/README.md) | [Regex Form Validator](./projects/day-28-regex-form-validator/README.md) | -| 29 | [Debugging and Testing Basics](./days/day-29-debugging-and-testing-basics/README.md) | [Tested Utility Functions](./projects/day-29-tested-utility-functions/README.md) | -| 30 | [Capstone Project](./days/day-30-capstone-project/README.md) | [Expense Tracker App](./projects/day-30-expense-tracker-app/README.md) | - ---- +| Week 1 | Fundamentals | Setup, Variables, Strings, Numbers, User Input | +| Week 2 | Data Structures and Control Flow | Lists, Tuples, Sets, Dictionaries, Conditionals, Loops | +| Week 3 | Functions and Robust Code | Functions, Type Hints, Scope, Error Handling, File I/O | +| Week 4 | Pythonic Code and OOP | Comprehensions, Lambda/Map/Filter, Modules, Classes, Inheritance, Dunder Methods | +| Week 5 | Advanced Python and Real Data | Decorators, Generators, Context Managers, Regex, JSON/CSV, APIs | +| Final Stretch | Professional Practices | Testing with pytest, Final CLI Todo App project | -## Projects +
-All 30 projects live in the [`projects/`](./projects) folder, one folder per day, each with its own `README.md` brief, starter `index.html`, `style.css`, and `script.js`. Projects are deliberately small, no more than a couple of hours each, so you build a daily habit instead of burning out on one big build. +Full day by day breakdown: -## Best Practices Followed In This Course +| Day | Title | Topic | +|---|---|---| +| 01 | Setup & Hello World | Fundamentals | +| 02 | Variables & Data Types | Fundamentals | +| 03 | Strings & String Methods | Fundamentals | +| 04 | Numbers & Math Operations | Fundamentals | +| 05 | User Input | Fundamentals | +| 06 | Lists | Data Structures | +| 07 | Tuples, Sets & Booleans | Data Structures | +| 08 | Dictionaries | Data Structures | +| 09 | Conditionals | Control Flow | +| 10 | for Loops | Control Flow | +| 11 | while Loops | Control Flow | +| 12 | Functions Basics | Functions | +| 13 | Function Arguments & Type Hints | Functions | +| 14 | Scope & Closures | Functions | +| 15 | Error Handling | Robust Code | +| 16 | File I/O | Robust Code | +| 17 | List Comprehensions | Pythonic Code | +| 18 | Lambda, Map, Filter & Sorted | Pythonic Code | +| 19 | Modules & Packages | Pythonic Code | +| 20 | OOP: Classes & Objects | Object Oriented Programming | +| 21 | OOP: Inheritance & Polymorphism | Object Oriented Programming | +| 22 | Dunder Methods | Object Oriented Programming | +| 23 | Decorators | Advanced Python | +| 24 | Generators & Iterators | Advanced Python | +| 25 | Context Managers | Advanced Python | +| 26 | Regular Expressions | Real World Data | +| 27 | JSON & CSV Data | Real World Data | +| 28 | Working with APIs | Real World Data | +| 29 | Testing with pytest | Professional Practices | +| 30 | Final Project: CLI Todo App | Professional Practices | + +## Authentication Model + +This project intentionally ships a login and signup screen that does **not** gate any content. It exists to demonstrate what an authenticated experience could look like in the UI, nothing more. -Every lesson in this course is written to match real, current JavaScript standards rather than outdated tutorials. Specifically, this course: +```text + ┌─────────────────────────┐ + │ Visitor opens the app │ + └────────────┬────────────┘ + │ + ┌───────────────┴────────────────┐ + │ │ + ▼ ▼ + ┌───────────────────────┐ ┌─────────────────────────┐ + │ Browses lessons freely │ │ Optionally signs up │ + │ No account needed │ │ or logs in (showcase) │ + └───────────┬───────────┘ └────────────┬─────────────┘ + │ │ + ▼ ▼ + ┌───────────────────────┐ ┌─────────────────────────┐ + │ Marks days complete │ │ Sees a personalized │ + │ (stored in browser) │ │ greeting in the navbar │ + └───────────┬───────────┘ └────────────┬─────────────┘ + │ │ + └────────────────┬─────────────────┘ + ▼ + ┌─────────────────────────┐ + │ Generates certificate │ + │ any time, no login, │ + │ no test score required │ + └─────────────────────────┘ +``` -- Uses **`let`** and **`const`** instead of `var`, and explains why -- Uses **strict equality (`===`)** instead of loose equality (`==`) -- Follows naming conventions from the **[Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript)** -- References the **[MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript)** and the **ECMAScript specification** as the source of truth -- Teaches **asynchronous JavaScript with Promises and `async`/`await`**, not only outdated callback patterns -- Encourages **small, pure, single responsibility functions** -- Introduces **semantic HTML** and accessible form patterns alongside JavaScript -- Avoids global variables and teaches proper **scope and module boundaries** +Key facts about the showcase auth: -## Frequently Asked Questions +- Accounts are stored in `localStorage` under the current browser only. There is no backend, no database and no network request involved. +- Signing up or logging in only changes the greeting shown in the navbar. It never unlocks or restricts any lesson. +- The certificate page never checks whether a user is logged in. -**Is this course really free?** -Yes. The entire course, including every lesson and every project, is open source under the MIT License. +## Certificate -**Do I need to know HTML and CSS first?** -Basic HTML and CSS helps for the DOM lessons starting on Day 12, but Days 1 to 11 only require JavaScript running in the browser console or Node.js. +The `/certificate` route renders a live SVG certificate that updates as you type your name, and can be exported to a PNG file with a single click. There is no login requirement and no minimum number of completed lessons. The certificate is available the moment you land on the page, because learning should be rewarded on your terms, not gated behind arbitrary checkpoints. -**Can I use this content to teach others?** -Yes, as long as you follow the terms of the [MIT License](./LICENSE), which includes keeping the original copyright notice. +## Accessibility and Design -**What if I get stuck on a project?** -Open an issue in this repository describing what you tried, and the community can help. You can also compare your solution against the lesson's code examples, which are all tested and correct. +- Semantic HTML throughout, including proper heading hierarchy inside rendered lessons. +- Keyboard accessible navigation links and buttons. +- Color palette inspired by the official Python brand colors (blue and yellow) on a high contrast dark theme for comfortable long reading sessions. +- Responsive layout with a collapsible sidebar drawer below 900px width. -**What comes after Day 30?** -After finishing this course you will be ready to learn a frontend framework such as React or Vue, or a backend runtime such as Node.js, with a solid JavaScript foundation already in place. +## Roadmap ---- +- [ ] Add optional dark and light theme toggle +- [ ] Add search across lessons +- [ ] Add code playground for running snippets in the browser +- [ ] Add downloadable PDF certificate in addition to PNG +- [ ] Add i18n support for translated lessons ## Contributing -Contributions are welcome and encouraged. Whether it is fixing a typo, improving an explanation, or adding a new project variation, please read the **[Contributing Guide](./CONTRIBUTING.md)** before opening a pull request. +Contributions are welcome. Please open an issue to discuss significant changes before submitting a pull request. -If this course helped you, consider: +1. Fork the repository +2. Create a feature branch: `git checkout -b feature/my-improvement` +3. Commit your changes: `git commit -m "Add my improvement"` +4. Push to your branch: `git push origin feature/my-improvement` +5. Open a pull request -- Starring this repository -- Sharing it with someone who is learning to code -- Opening an issue if you find an error, no matter how small +Please run `npm run lint` and `npm run build` locally before submitting a pull request. Both checks also run automatically in CI on every pull request. ## License -This project is licensed under the **[MIT License](./LICENSE)**. You are free to use, copy, modify, and distribute this material with attribution. - ---- +This project is licensed under the [MIT License](./LICENSE). Course content is adapted from the original Python in 30 Days curriculum and remains free for personal and educational use.
-**[Begin Day 1: Introduction and Setup →](./days/day-01-introduction-and-setup/README.md)** - -Made for everyone learning to code. If this repository helps you, please consider giving it a star. +Made for learners who just want to start writing Python, without filling out a form first.
diff --git a/docs/architecture.svg b/docs/architecture.svg new file mode 100644 index 0000000..a863cb0 --- /dev/null +++ b/docs/architecture.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + Application Architecture + + + + Content Source + 30x lesson.md + 30x solution.py + + + Vite Build + import.meta.glob + raw text bundling + + + Static Bundle + HTML / CSS / JS + dist/ + + + + + + GitHub Actions + lint, build, upload + artifact + + + + + + + GitHub Pages + Live site, no login needed + + + + + React Runtime + Router, Context, + Markdown renderer + + + + + + diff --git a/docs/banner.svg b/docs/banner.svg new file mode 100644 index 0000000..5ed2bc5 --- /dev/null +++ b/docs/banner.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Python in 30 Days + FROM ZERO TO CONFIDENT DEVELOPER + + + 30 + LESSONS + 30 + PROJECTS + + + 300+ + EXAMPLES + 0 + to + PRO + SKILL LEVEL + Open Source · Community Driven · Beginner Friendly · PEP 8 Compliant + diff --git a/index.html b/index.html new file mode 100644 index 0000000..b565f94 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + + + + Python in 30 Days | Interactive Learning Platform + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..d581719 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "python-30-days-react", + "private": true, + "version": "1.0.0", + "type": "module", + "description": "Python in 30 Days - interactive learning platform built with React, TypeScript and Vite", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "react-markdown": "^8.0.7", + "remark-gfm": "^3.0.1", + "react-syntax-highlighter": "^15.5.0" + }, + "devDependencies": { + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@types/react-syntax-highlighter": "^15.5.13", + "@typescript-eslint/eslint-plugin": "^8.5.0", + "@typescript-eslint/parser": "^8.5.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.11", + "gh-pages": "^6.1.1", + "typescript": "^5.5.4", + "vite": "^5.4.6" + } +} diff --git a/public/banner.svg b/public/banner.svg new file mode 100644 index 0000000..5ed2bc5 --- /dev/null +++ b/public/banner.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Python in 30 Days + FROM ZERO TO CONFIDENT DEVELOPER + + + 30 + LESSONS + 30 + PROJECTS + + + 300+ + EXAMPLES + 0 + to + PRO + SKILL LEVEL + Open Source · Community Driven · Beginner Friendly · PEP 8 Compliant + diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..e97d121 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..6a262cd --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,23 @@ +import { Routes, Route } from 'react-router-dom' +import Layout from './components/Layout' +import Home from './pages/Home' +import DayPage from './pages/DayPage' +import Login from './pages/Login' +import Signup from './pages/Signup' +import CertificatePage from './pages/CertificatePage' +import NotFound from './pages/NotFound' + +export default function App() { + return ( + + }> + } /> + } /> + } /> + } /> + } /> + } /> + + + ) +} diff --git a/src/components/Certificate.tsx b/src/components/Certificate.tsx new file mode 100644 index 0000000..533e25c --- /dev/null +++ b/src/components/Certificate.tsx @@ -0,0 +1,81 @@ +import { forwardRef } from 'react' + +interface CertificateProps { + name: string + date: string +} + +const Certificate = forwardRef(({ name, date }, ref) => { + const displayName = name.trim() || 'Your Name Here' + + return ( + + + + + + + + + + + + + + + + + + CERTIFICATE OF COMPLETION + + + + Python in 30 Days + + + + This certifies that + + + + {displayName} + + + + + + has completed the 30 day Python programming curriculum, covering + + + fundamentals, data structures, object oriented programming and real world projects. + + + + Issued on {date} + + + + + + 🐍 + + + + + python-30-days-react + + + No account required + + + ) +}) + +Certificate.displayName = 'Certificate' + +export default Certificate diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx new file mode 100644 index 0000000..8400469 --- /dev/null +++ b/src/components/CodeBlock.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' +import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism' + +interface CodeBlockProps { + className?: string + children?: React.ReactNode + inline?: boolean +} + +export default function CodeBlock({ className, children, inline }: CodeBlockProps) { + const match = /language-(\w+)/.exec(className || '') + const code = String(children ?? '').replace(/\n$/, '') + + if (inline || !match) { + return {code} + } + + return ( + + {code} + + ) +} diff --git a/src/components/Footer.css b/src/components/Footer.css new file mode 100644 index 0000000..be1c33e --- /dev/null +++ b/src/components/Footer.css @@ -0,0 +1,20 @@ +.footer { + margin-top: auto; + border-top: 1px solid var(--border); + padding: 1.5rem; + text-align: center; + color: var(--text-faint); + font-size: 0.8rem; +} + +.footer__inner { + max-width: 760px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.footer__meta { + color: var(--text-faint); +} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..5b0bb14 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,18 @@ +import './Footer.css' + +export default function Footer() { + return ( +
+
+

+ Python in 30 Days is a free, open source, interactive learning platform. Built with React, + TypeScript and Vite. +

+

+ No login required to learn. No login required for your certificate. Content is licensed under + MIT. +

+
+
+ ) +} diff --git a/src/components/Layout.css b/src/components/Layout.css new file mode 100644 index 0000000..843b660 --- /dev/null +++ b/src/components/Layout.css @@ -0,0 +1,30 @@ +.layout { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.layout__body { + display: flex; + flex: 1; +} + +.layout__content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.layout__overlay { + position: fixed; + inset: 64px 0 0 0; + background: rgba(0, 0, 0, 0.4); + z-index: 38; +} + +@media (min-width: 901px) { + .layout__overlay { + display: none; + } +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..eae9cf9 --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,24 @@ +import { useState } from 'react' +import { Outlet } from 'react-router-dom' +import Navbar from './Navbar' +import Sidebar from './Sidebar' +import Footer from './Footer' +import './Layout.css' + +export default function Layout() { + const [menuOpen, setMenuOpen] = useState(false) + + return ( +
+ setMenuOpen((v) => !v)} /> +
+ setMenuOpen(false)} /> + {menuOpen &&
setMenuOpen(false)} />} +
+ +
+
+
+
+ ) +} diff --git a/src/components/Navbar.css b/src/components/Navbar.css new file mode 100644 index 0000000..87b5874 --- /dev/null +++ b/src/components/Navbar.css @@ -0,0 +1,163 @@ +.navbar { + position: sticky; + top: 0; + z-index: 40; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + height: 64px; + padding: 0 1.25rem; + background: var(--surface); + border-bottom: 1px solid var(--border); + backdrop-filter: blur(8px); +} + +.navbar__left { + display: flex; + align-items: center; + gap: 0.75rem; + min-width: 0; +} + +.navbar__menu-btn { + display: none; + flex-direction: column; + justify-content: center; + gap: 4px; + width: 34px; + height: 34px; + border-radius: 8px; + border: 1px solid var(--border); + background: transparent; + cursor: pointer; +} + +.navbar__menu-btn span { + display: block; + width: 18px; + height: 2px; + margin: 0 auto; + background: var(--text); + border-radius: 2px; +} + +.navbar__brand { + display: flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + color: var(--text); + font-weight: 700; + white-space: nowrap; +} + +.navbar__logo { + font-size: 1.5rem; +} + +.navbar__title em { + font-style: normal; + color: var(--accent); +} + +.navbar__links { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.navbar__links a { + color: var(--text-muted); + text-decoration: none; + font-weight: 600; + font-size: 0.92rem; + padding: 0.4rem 0; + border-bottom: 2px solid transparent; + transition: color 0.15s ease, border-color 0.15s ease; +} + +.navbar__links a:hover { + color: var(--text); +} + +.navbar__links a.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.navbar__right { + display: flex; + align-items: center; + gap: 1rem; +} + +.navbar__progress { + display: none; + align-items: center; + gap: 0.5rem; + font-size: 0.8rem; + color: var(--text-muted); +} + +.navbar__progress-track { + width: 90px; + height: 6px; + border-radius: 999px; + background: var(--border); + overflow: hidden; +} + +.navbar__progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--python-blue), var(--python-yellow)); + transition: width 0.3s ease; +} + +.navbar__auth { + display: flex; + gap: 0.5rem; +} + +.navbar__user { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.navbar__avatar { + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--accent); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + font-weight: 700; +} + +.navbar__username { + font-size: 0.85rem; + font-weight: 600; + max-width: 110px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (min-width: 900px) { + .navbar__progress { + display: flex; + } +} + +@media (max-width: 900px) { + .navbar__menu-btn { + display: flex; + } + .navbar__links { + display: none; + } +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..82833bf --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,67 @@ +import { Link, NavLink } from 'react-router-dom' +import { useAuth } from '../context/AuthContext' +import { useProgress } from '../context/ProgressContext' +import './Navbar.css' + +export default function Navbar({ onMenuClick }: { onMenuClick: () => void }) { + const { user, logout } = useAuth() + const { percentComplete } = useProgress() + + return ( +
+
+ + + 🐍 + + Python in 30 Days + + +
+ + + +
+
+
+
+
+ {percentComplete}% +
+ + {user ? ( +
+ {user.name.charAt(0).toUpperCase()} + {user.name} + +
+ ) : ( +
+ + Log in + + + Sign up + +
+ )} +
+
+ ) +} diff --git a/src/components/Sidebar.css b/src/components/Sidebar.css new file mode 100644 index 0000000..e1a3f94 --- /dev/null +++ b/src/components/Sidebar.css @@ -0,0 +1,111 @@ +.sidebar { + width: 280px; + flex-shrink: 0; + border-right: 1px solid var(--border); + background: var(--surface); + height: calc(100vh - 64px); + position: sticky; + top: 64px; + overflow-y: auto; + padding-bottom: 2rem; +} + +.sidebar__summary { + padding: 1rem 1.1rem; + border-bottom: 1px solid var(--border); +} + +.sidebar__summary-text { + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: 0.5rem; +} + +.sidebar__summary-text strong { + color: var(--text); +} + +.sidebar__summary-track { + height: 6px; + border-radius: 999px; + background: var(--border); + overflow: hidden; +} + +.sidebar__summary-fill { + height: 100%; + background: linear-gradient(90deg, var(--python-blue), var(--python-yellow)); +} + +.sidebar__group-title { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-faint); + padding: 1rem 1.1rem 0.4rem; + font-weight: 700; +} + +.sidebar__item { + display: flex; + align-items: center; + gap: 0.65rem; + padding: 0.5rem 1.1rem; + text-decoration: none; + color: var(--text-muted); + font-size: 0.88rem; + border-left: 3px solid transparent; + transition: background 0.12s ease, color 0.12s ease; +} + +.sidebar__item:hover { + background: var(--surface-hover); + color: var(--text); +} + +.sidebar__item--active { + color: var(--accent); + border-left-color: var(--accent); + background: var(--surface-hover); + font-weight: 600; +} + +.sidebar__item-check { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 50%; + background: var(--border); + font-size: 0.7rem; + flex-shrink: 0; + color: var(--text-muted); +} + +.sidebar__item--active .sidebar__item-check { + background: var(--accent); + color: #fff; +} + +.sidebar__item-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 900px) { + .sidebar { + position: fixed; + left: -300px; + top: 64px; + z-index: 39; + width: 280px; + background: var(--surface); + transition: left 0.2s ease; + box-shadow: 4px 0 16px rgba(0, 0, 0, 0.15); + } + .sidebar--open { + left: 0; + } +} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..1223b8c --- /dev/null +++ b/src/components/Sidebar.tsx @@ -0,0 +1,49 @@ +import { NavLink } from 'react-router-dom' +import { CURRICULUM, TOPICS } from '../data/curriculum' +import { useProgress } from '../context/ProgressContext' +import './Sidebar.css' + +interface SidebarProps { + open: boolean + onNavigate?: () => void +} + +export default function Sidebar({ open, onNavigate }: SidebarProps) { + const { isComplete, completedCount, percentComplete } = useProgress() + + return ( + + ) +} diff --git a/src/content/day-01/lesson.md b/src/content/day-01/lesson.md new file mode 100644 index 0000000..89d2044 --- /dev/null +++ b/src/content/day-01/lesson.md @@ -0,0 +1,134 @@ +# Day 01 🐍 - Setup & Hello World + +--- + +## Overview + +Install Python, write your first script, and understand how Python programs run. + +**What you will learn today:** + +- Installing Python 3.10+ +- Running your first script +- The `print()` function +- Comments in Python +- Python interactive shell (REPL) +- How Python executes code + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `print()` | Outputs text to the screen. The most fundamental Python function. | +| `# comments` | Lines starting with `#` are ignored by Python. They explain code to humans. | +| `Python REPL` | Type `python3` in your terminal to enter an interactive Python session. | + +--- + +## Code Examples + +### Your First Python Program + +```python +# This is a comment - Python ignores it +print("Hello, World!") +print("My name is Python and I am 30 years old.") +``` + +### print() with multiple arguments + +```python +# print() can take multiple arguments separated by commas +print("Python", "is", "awesome") # Output: Python is awesome + +# You can change the separator +print("2024", "01", "15", sep="-") # Output: 2024-01-15 + +# You can change the line ending +print("Loading", end="...") # Output: Loading... +print("Done!") # Output: Done! +``` + +### Python as a calculator (REPL) + +```python +# In your terminal, type: python3 +# Then try these: +>>> 2 + 2 +4 +>>> 10 / 3 +3.3333333333333335 +>>> 2 ** 10 +1024 +>>> exit() # Leave the REPL +``` + +--- + +## Today's Project: Personal Info Card + +> Build a formatted personal information card that displays your name, age, city, and a fun fact. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 01 Project: Personal Info Card +=================================== +Print a formatted card showing personal information. +""" + +# Personal information (change these to your own!) +name = "Alice Johnson" +age = 28 +city = "San Francisco" +occupation = "Software Developer" +fun_fact = "I can solve a Rubik's cube in under 2 minutes" +favorite_language = "Python" + +# Print a formatted info card +print("=" * 45) +print(" PERSONAL INFO CARD") +print("=" * 45) +print(f" Name : {name}") +print(f" Age : {age}") +print(f" City : {city}") +print(f" Occupation : {occupation}") +print(f" Fun Fact : {fun_fact}") +print(f" Fav Language: {favorite_language}") +print("=" * 45) +print(" Happy coding! 🐍") +print("=" * 45) +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can answer these: + +1. How do you run a Python script from the terminal? +2. What is the difference between `print("a", "b")` and `print("a" + "b")`? +3. Can you explain what happens when Python reads your file top to bottom? +4. What does the `#` symbol do in Python? +5. How do you enter and exit the Python REPL? + +--- + +## Common Mistakes to Avoid + +```python +# WRONG - mixing indentation +if True: + print("hello") + print("world") # IndentationError + +# CORRECT - consistent 4-space indentation +if True: + print("hello") + print("world") +``` + +--- diff --git a/src/content/day-01/solution.py b/src/content/day-01/solution.py new file mode 100644 index 0000000..1d6a6b7 --- /dev/null +++ b/src/content/day-01/solution.py @@ -0,0 +1,27 @@ +""" +Day 01 Project: Personal Info Card +=================================== +Print a formatted card showing personal information. +""" + +# Personal information (change these to your own!) +name = "Alice Johnson" +age = 28 +city = "San Francisco" +occupation = "Software Developer" +fun_fact = "I can solve a Rubik's cube in under 2 minutes" +favorite_language = "Python" + +# Print a formatted info card +print("=" * 45) +print(" PERSONAL INFO CARD") +print("=" * 45) +print(f" Name : {name}") +print(f" Age : {age}") +print(f" City : {city}") +print(f" Occupation : {occupation}") +print(f" Fun Fact : {fun_fact}") +print(f" Fav Language: {favorite_language}") +print("=" * 45) +print(" Happy coding! 🐍") +print("=" * 45) diff --git a/src/content/day-02/lesson.md b/src/content/day-02/lesson.md new file mode 100644 index 0000000..39ad282 --- /dev/null +++ b/src/content/day-02/lesson.md @@ -0,0 +1,170 @@ +# Day 02 📦 - Variables & Data Types + +--- + +## Overview + +Learn how Python stores information using variables and explore the four core data types. + +**What you will learn today:** + +- What variables are and why we need them +- The four core types: `int`, `float`, `str`, `bool` +- The `type()` function +- Type conversion (casting) +- Variable naming rules and conventions +- Multiple assignment + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `Variables` | Names that point to values stored in memory. Python is dynamically typed - you do not declare the type. | +| `int` | Whole numbers with no decimal point. Example: `42`, `-7`, `0` | +| `float` | Numbers with a decimal point. Example: `3.14`, `-0.5`, `2.0` | +| `str` | Text, enclosed in single or double quotes. Example: `'hello'`, `"world"` | +| `bool` | Either `True` or `False`. Used for logic and conditions. | + +--- + +## Code Examples + +### Creating and using variables + +```python +# Create variables by simply assigning a value +name = "Alice" +age = 28 +height = 5.6 +is_student = False + +# Use the variable anywhere after it is defined +print(name) # Alice +print(age + 2) # 30 +print(height) # 5.6 +print(is_student)# False +``` + +### Checking and converting types + +```python +# Check the type of any variable with type() +x = 42 +print(type(x)) # + +price = 9.99 +print(type(price)) # + +label = "sale" +print(type(label)) # + +# Convert between types +num_str = "100" +num_int = int(num_str) # "100" -> 100 +print(type(num_int)) # + +ratio = float(num_int) # 100 -> 100.0 +print(ratio) # 100.0 + +# Convert number to string +code = str(42) # 42 -> "42" +print("Your code is: " + code) +``` + +### Multiple assignment + +```python +# Assign multiple variables on one line +x, y, z = 1, 2, 3 +print(x, y, z) # 1 2 3 + +# Assign the same value to multiple variables +a = b = c = 0 +print(a, b, c) # 0 0 0 + +# Swap values without a temporary variable +x, y = y, x +print(x, y) # 2 1 +``` + +--- + +## Today's Project: Type Explorer + +> Build a program that accepts a value from the user, identifies its type, and converts it to all compatible types. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 02 Project: Type Explorer +============================== +Accept user input and explore data type conversions. +""" + +print("=== TYPE EXPLORER ===") +print("Enter a value and we'll explore its type.\n") + +user_input = input("Enter any value: ") + +print(f"\nYou entered : {user_input!r}") +print(f"Type : {type(user_input).__name__}") +print() +print("--- Conversion Attempts ---") + +# Try converting to int +try: + as_int = int(user_input) + print(f"As int : {as_int}") +except ValueError: + print("As int : Not possible") + +# Try converting to float +try: + as_float = float(user_input) + print(f"As float : {as_float}") +except ValueError: + print("As float : Not possible") + +# Always possible - every value has a string form +as_str = str(user_input) +print(f"As string : {as_str!r}") + +# Boolean interpretation +as_bool = bool(user_input) +print(f"As bool : {as_bool}") +print() +print(f"Length (chars): {len(user_input)}") +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can answer these: + +1. How do you run a Python script from the terminal? +2. What is the difference between `print("a", "b")` and `print("a" + "b")`? +3. Can you explain what happens when Python reads your file top to bottom? +4. What does the `#` symbol do in Python? +5. How do you enter and exit the Python REPL? + +--- + +## Common Mistakes to Avoid + +```python +# WRONG - mixing indentation +if True: + print("hello") + print("world") # IndentationError + +# CORRECT - consistent 4-space indentation +if True: + print("hello") + print("world") +``` + +--- diff --git a/src/content/day-02/solution.py b/src/content/day-02/solution.py new file mode 100644 index 0000000..27aec69 --- /dev/null +++ b/src/content/day-02/solution.py @@ -0,0 +1,39 @@ +""" +Day 02 Project: Type Explorer +============================== +Accept user input and explore data type conversions. +""" + +print("=== TYPE EXPLORER ===") +print("Enter a value and we'll explore its type.\n") + +user_input = input("Enter any value: ") + +print(f"\nYou entered : {user_input!r}") +print(f"Type : {type(user_input).__name__}") +print() +print("--- Conversion Attempts ---") + +# Try converting to int +try: + as_int = int(user_input) + print(f"As int : {as_int}") +except ValueError: + print("As int : Not possible") + +# Try converting to float +try: + as_float = float(user_input) + print(f"As float : {as_float}") +except ValueError: + print("As float : Not possible") + +# Always possible - every value has a string form +as_str = str(user_input) +print(f"As string : {as_str!r}") + +# Boolean interpretation +as_bool = bool(user_input) +print(f"As bool : {as_bool}") +print() +print(f"Length (chars): {len(user_input)}") diff --git a/src/content/day-03/lesson.md b/src/content/day-03/lesson.md new file mode 100644 index 0000000..cd7040c --- /dev/null +++ b/src/content/day-03/lesson.md @@ -0,0 +1,185 @@ +# Day 03 ✍️ - Strings & String Methods + +--- + +## Overview + +Master Python strings - one of the most used data types - including slicing, formatting, and 15+ essential methods. + +**What you will learn today:** + +- String creation and indexing +- String slicing `[start:stop:step]` +- f-strings (formatted string literals) +- Essential string methods +- `str.format()` and `%` formatting +- Multiline strings with triple quotes + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `Indexing` | Access individual characters with `[index]`. First character is index `0`. Negative indices count from the end. | +| `Slicing` | Extract substrings with `[start:stop:step]`. The stop index is not included. | +| `f-strings` | The modern way to embed variables in strings: `f'Hello, {name}!'`. Introduced in Python 3.6. | +| `Immutability` | Strings cannot be changed after creation. Methods always return a *new* string. | + +--- + +## Code Examples + +### Indexing and slicing + +```python +text = "Python" +# P y t h o n +# Index: 0 1 2 3 4 5 +# -6 -5 -4 -3 -2 -1 + +print(text[0]) # P (first character) +print(text[-1]) # n (last character) +print(text[1:4]) # yth (index 1 up to but not including 4) +print(text[:3]) # Pyt (from start to index 3) +print(text[3:]) # hon (from index 3 to end) +print(text[::-1]) # nohtyP (reversed!) +``` + +### Essential string methods + +```python +name = " alice johnson " + +print(name.strip()) # "alice johnson" (remove whitespace) +print(name.strip().title()) # "Alice Johnson" (capitalize each word) +print(name.strip().upper()) # "ALICE JOHNSON" (all uppercase) +print(name.strip().lower()) # "alice johnson" (all lowercase) + +sentence = "I love Python. Python is great." +print(sentence.count("Python")) # 2 +print(sentence.replace("Python", "code")) # "I love code. code is great." +print(sentence.split(".")) # ['I love Python', ' Python is great', ''] +print(" ".join(["one", "two", "three"])) # "one two three" + +email = "user@example.com" +print(email.startswith("user")) # True +print(email.endswith(".com")) # True +print(email.find("@")) # 4 (index of @) +``` + +### f-strings - the modern way + +```python +name = "Alice" +age = 28 +score = 9.5 + +# Basic usage +print(f"My name is {name} and I am {age} years old.") + +# Expressions inside f-strings +print(f"Next year I will be {age + 1}.") + +# Formatting numbers +pi = 3.14159265 +print(f"Pi to 2 decimal places: {pi:.2f}") +print(f"Pi as percentage: {pi * 100:.1f}%") + +# Padding and alignment +for i in range(1, 4): + print(f"Item {i:>3}: {'*' * i}") +# Item 1: * +# Item 2: ** +# Item 3: *** +``` + +--- + +## Today's Project: Mad Libs Generator + +> Ask the user for nouns, verbs, and adjectives, then slot them into a funny story template using string methods and f-strings. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 03 Project: Mad Libs Generator +==================================== +Collect words from the user and build a funny story. +""" + +print("=" * 50) +print(" MAD LIBS STORY GENERATOR") +print("=" * 50) +print("\nAnswer the prompts below to build your story!\n") + +# Collect words from the user +adjective1 = input("Enter an adjective (e.g. fluffy): ").strip() +noun1 = input("Enter a noun (e.g. elephant): ").strip() +verb_past = input("Enter a verb in past tense (e.g. jumped): ").strip() +place = input("Enter a place (e.g. the library): ").strip() +adjective2 = input("Enter another adjective (e.g. mysterious): ").strip() +noun2 = input("Enter another noun (e.g. sandwich): ").strip() +number = input("Enter a number: ").strip() +verb_ing = input("Enter a verb ending in -ing (e.g. dancing): ").strip() + +# Normalize capitalization +adjective1 = adjective1.lower() +noun1 = noun1.lower() +verb_past = verb_past.lower() +place = place.lower() +adjective2 = adjective2.lower() +noun2 = noun2.lower() +verb_ing = verb_ing.lower() + +# Build the story using an f-string template +story = f""" +{"=" * 50} + YOUR MAD LIBS STORY +{"=" * 50} + +Once upon a time, a {adjective1} {noun1} {verb_past} all the +way to {place}. Nobody expected to find a {adjective2} +{noun2} there, but there it was - all {number} of them! + +The crowd began {verb_ing} immediately. It was the most +extraordinary sight anyone in {place} had ever seen. +The {noun1} smiled, took a bow, and disappeared forever. + +THE END. +{"=" * 50} +""" + +print(story) +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can answer these: + +1. How do you run a Python script from the terminal? +2. What is the difference between `print("a", "b")` and `print("a" + "b")`? +3. Can you explain what happens when Python reads your file top to bottom? +4. What does the `#` symbol do in Python? +5. How do you enter and exit the Python REPL? + +--- + +## Common Mistakes to Avoid + +```python +# WRONG - mixing indentation +if True: + print("hello") + print("world") # IndentationError + +# CORRECT - consistent 4-space indentation +if True: + print("hello") + print("world") +``` + +--- diff --git a/src/content/day-03/solution.py b/src/content/day-03/solution.py new file mode 100644 index 0000000..28d99f5 --- /dev/null +++ b/src/content/day-03/solution.py @@ -0,0 +1,49 @@ +""" +Day 03 Project: Mad Libs Generator +==================================== +Collect words from the user and build a funny story. +""" + +print("=" * 50) +print(" MAD LIBS STORY GENERATOR") +print("=" * 50) +print("\nAnswer the prompts below to build your story!\n") + +# Collect words from the user +adjective1 = input("Enter an adjective (e.g. fluffy): ").strip() +noun1 = input("Enter a noun (e.g. elephant): ").strip() +verb_past = input("Enter a verb in past tense (e.g. jumped): ").strip() +place = input("Enter a place (e.g. the library): ").strip() +adjective2 = input("Enter another adjective (e.g. mysterious): ").strip() +noun2 = input("Enter another noun (e.g. sandwich): ").strip() +number = input("Enter a number: ").strip() +verb_ing = input("Enter a verb ending in -ing (e.g. dancing): ").strip() + +# Normalize capitalization +adjective1 = adjective1.lower() +noun1 = noun1.lower() +verb_past = verb_past.lower() +place = place.lower() +adjective2 = adjective2.lower() +noun2 = noun2.lower() +verb_ing = verb_ing.lower() + +# Build the story using an f-string template +story = f""" +{"=" * 50} + YOUR MAD LIBS STORY +{"=" * 50} + +Once upon a time, a {adjective1} {noun1} {verb_past} all the +way to {place}. Nobody expected to find a {adjective2} +{noun2} there, but there it was - all {number} of them! + +The crowd began {verb_ing} immediately. It was the most +extraordinary sight anyone in {place} had ever seen. +The {noun1} smiled, took a bow, and disappeared forever. + +THE END. +{"=" * 50} +""" + +print(story) diff --git a/src/content/day-04/lesson.md b/src/content/day-04/lesson.md new file mode 100644 index 0000000..2347175 --- /dev/null +++ b/src/content/day-04/lesson.md @@ -0,0 +1,199 @@ +# Day 04 🔢 - Numbers & Math Operations + +--- + +## Overview + +Work with integers, floats, and Python's math module to perform calculations from basic arithmetic to trigonometry. + +**What you will learn today:** + +- Arithmetic operators: `+`, `-`, `*`, `/`, `//`, `%`, `**` +- Operator precedence (PEMDAS/BODMAS) +- Integer vs float division +- The `math` module +- `round()`, `abs()`, `min()`, `max()` +- Augmented assignment operators + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| ``//` Floor Division` | Divides and rounds down to the nearest integer. `7 // 2` gives `3`, not `3.5`. | +| ``%` Modulo` | Returns the remainder after division. `7 % 2` gives `1`. Great for checking if a number is even. | +| ``**` Exponentiation` | Raises to a power. `2 ** 8` gives `256`. Much more readable than `pow(2, 8)`. | +| `Augmented assignment` | Shortcuts like `x += 5` are equivalent to `x = x + 5`. | + +--- + +## Code Examples + +### All arithmetic operators + +```python +a = 17 +b = 5 + +print(a + b) # 22 Addition +print(a - b) # 12 Subtraction +print(a * b) # 85 Multiplication +print(a / b) # 3.4 True division (always float) +print(a // b) # 3 Floor division (rounds down) +print(a % b) # 2 Modulo (remainder) +print(a ** b) # 1419857 Exponentiation + +# Augmented assignment +score = 100 +score += 10 # score = 110 +score -= 5 # score = 105 +score *= 2 # score = 210 +score //= 3 # score = 70 +print(score) # 70 +``` + +### The math module + +```python +import math + +print(math.pi) # 3.141592653589793 +print(math.e) # 2.718281828459045 + +print(math.sqrt(144)) # 12.0 +print(math.ceil(4.1)) # 5 (round up) +print(math.floor(4.9)) # 4 (round down) +print(math.factorial(5)) # 120 (5!) +print(math.log(100, 10)) # 2.0 (log base 10 of 100) +print(math.sin(math.pi/2)) # 1.0 (sin of 90 degrees) + +# Practical: area of a circle +radius = 7 +area = math.pi * radius ** 2 +print(f"Area: {area:.2f}") # Area: 153.94 +``` + +--- + +## Today's Project: Scientific Calculator + +> Build a CLI calculator that handles all arithmetic operations plus square root, power, and percentage calculations. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 04 Project: Scientific Calculator +======================================= +A command-line calculator with multiple operations. +""" +import math + +def add(a: float, b: float) -> float: + """Return the sum of a and b.""" + return a + b + +def subtract(a: float, b: float) -> float: + """Return a minus b.""" + return a - b + +def multiply(a: float, b: float) -> float: + """Return the product of a and b.""" + return a * b + +def divide(a: float, b: float) -> float: + """Return a divided by b. Raises ValueError if b is zero.""" + if b == 0: + raise ValueError("Cannot divide by zero.") + return a / b + +def power(base: float, exp: float) -> float: + """Return base raised to the power of exp.""" + return base ** exp + +def square_root(n: float) -> float: + """Return the square root of n. Raises ValueError if n < 0.""" + if n < 0: + raise ValueError("Cannot take square root of a negative number.") + return math.sqrt(n) + +def main() -> None: + """Run the interactive calculator.""" + operations = { + "1": ("Addition (+)", add, 2), + "2": ("Subtraction (-)", subtract, 2), + "3": ("Multiplication (*)", multiply, 2), + "4": ("Division (/)", divide, 2), + "5": ("Power (^)", power, 2), + "6": ("Square Root (√)", square_root, 1), + } + + print("=" * 40) + print(" SCIENTIFIC CALCULATOR") + print("=" * 40) + + while True: + print("\nSelect operation:") + for key, (name, _, _) in operations.items(): + print(f" {key}. {name}") + print(" q. Quit") + + choice = input("\nYour choice: ").strip().lower() + + if choice == "q": + print("\nGoodbye! Keep calculating. 🔢") + break + + if choice not in operations: + print("Invalid choice. Please try again.") + continue + + name, func, num_args = operations[choice] + + try: + if num_args == 2: + a = float(input("Enter first number: ")) + b = float(input("Enter second number: ")) + result = func(a, b) + print(f"\nResult: {a} {name[name.index('(')+1]} {b} = {result}") + else: + a = float(input("Enter number: ")) + result = func(a) + print(f"\nResult: {result:.6f}") + except ValueError as e: + print(f"Error: {e}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can answer these: + +1. How do you run a Python script from the terminal? +2. What is the difference between `print("a", "b")` and `print("a" + "b")`? +3. Can you explain what happens when Python reads your file top to bottom? +4. What does the `#` symbol do in Python? +5. How do you enter and exit the Python REPL? + +--- + +## Common Mistakes to Avoid + +```python +# WRONG - mixing indentation +if True: + print("hello") + print("world") # IndentationError + +# CORRECT - consistent 4-space indentation +if True: + print("hello") + print("world") +``` + +--- diff --git a/src/content/day-04/solution.py b/src/content/day-04/solution.py new file mode 100644 index 0000000..6f7b885 --- /dev/null +++ b/src/content/day-04/solution.py @@ -0,0 +1,91 @@ +""" +Day 04 Project: Scientific Calculator +======================================= +A command-line calculator with multiple operations. +""" +import math + + +def add(a: float, b: float) -> float: + """Return the sum of a and b.""" + return a + b + + +def subtract(a: float, b: float) -> float: + """Return a minus b.""" + return a - b + + +def multiply(a: float, b: float) -> float: + """Return the product of a and b.""" + return a * b + + +def divide(a: float, b: float) -> float: + """Return a divided by b. Raises ValueError if b is zero.""" + if b == 0: + raise ValueError("Cannot divide by zero.") + return a / b + + +def power(base: float, exp: float) -> float: + """Return base raised to the power of exp.""" + return base ** exp + + +def square_root(n: float) -> float: + """Return the square root of n. Raises ValueError if n < 0.""" + if n < 0: + raise ValueError("Cannot take square root of a negative number.") + return math.sqrt(n) + + +def main() -> None: + """Run the interactive calculator.""" + operations = { + "1": ("Addition (+)", add, 2), + "2": ("Subtraction (-)", subtract, 2), + "3": ("Multiplication (*)", multiply, 2), + "4": ("Division (/)", divide, 2), + "5": ("Power (^)", power, 2), + "6": ("Square Root (√)", square_root, 1), + } + + print("=" * 40) + print(" SCIENTIFIC CALCULATOR") + print("=" * 40) + + while True: + print("\nSelect operation:") + for key, (name, _, _) in operations.items(): + print(f" {key}. {name}") + print(" q. Quit") + + choice = input("\nYour choice: ").strip().lower() + + if choice == "q": + print("\nGoodbye! Keep calculating. 🔢") + break + + if choice not in operations: + print("Invalid choice. Please try again.") + continue + + name, func, num_args = operations[choice] + + try: + if num_args == 2: + a = float(input("Enter first number: ")) + b = float(input("Enter second number: ")) + result = func(a, b) + print(f"\nResult: {a} {name[name.index('(')+1]} {b} = {result}") + else: + a = float(input("Enter number: ")) + result = func(a) + print(f"\nResult: {result:.6f}") + except ValueError as e: + print(f"Error: {e}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-05/lesson.md b/src/content/day-05/lesson.md new file mode 100644 index 0000000..493ee5e --- /dev/null +++ b/src/content/day-05/lesson.md @@ -0,0 +1,165 @@ +# Day 05 ⌨️ - User Input + +--- + +## Overview + +Learn to make your Python programs interactive by reading input from the user, and safely converting that input to the right data type. + +**What you will learn today:** + +- The `input()` function +- Reading strings from the terminal +- Converting input to `int` or `float` +- Building interactive CLI programs +- Handling invalid input gracefully + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `input()` | Pauses the program and waits for the user to type something and press Enter. Always returns a `str`. | +| `Type casting` | Since `input()` always returns a string, use `int()` or `float()` to convert before doing math. | +| `prompt` | The optional string passed to `input()` is displayed before the cursor: `input('Your age: ')`. | + +--- + +## Code Examples + +### Basic input usage + +```python +# input() returns a string - always +name = input("What is your name? ") +print(f"Hello, {name}!") + +# Convert to a number before arithmetic +age_str = input("How old are you? ") +age = int(age_str) +print(f"In 10 years, you will be {age + 10}.") + +# One-liner conversion +price = float(input("Enter price: ")) +tax = price * 0.1 +print(f"Tax: ${tax:.2f} Total: ${price + tax:.2f}") +``` + +### Safe input with error handling + +```python +def get_integer(prompt: str) -> int: + """Ask for input until the user enters a valid integer.""" + while True: + try: + return int(input(prompt)) + except ValueError: + print("Please enter a whole number.") + +age = get_integer("Enter your age: ") +print(f"Age entered: {age}") +``` + +--- + +## Today's Project: Interactive Quiz + +> Build a 5-question multiple-choice quiz that collects answers, checks them, and shows a final score. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 05 Project: Interactive Quiz +================================== +A multiple-choice quiz with scoring. +""" + +QUESTIONS = [ + { + "question": "What does CPU stand for?", + "options": ["A) Central Processing Unit", "B) Computer Personal Unit", "C) Core Processing Utility"], + "answer": "A", + }, + { + "question": "How many bits are in a byte?", + "options": ["A) 4", "B) 8", "C) 16"], + "answer": "B", + }, + { + "question": "Which language was Python named after?", + "options": ["A) A snake", "B) A Greek god", "C) Monty Python (the comedy group)"], + "answer": "C", + }, + { + "question": "What does HTML stand for?", + "options": ["A) HyperText Markup Language", "B) High Transfer Markup Layer", "C) HyperText Main Language"], + "answer": "A", + }, + { + "question": "Which symbol is used for comments in Python?", + "options": ["A) //", "B) /*", "C) #"], + "answer": "C", + }, +] + +def run_quiz(questions: list[dict]) -> int: + """Run through questions and return the number of correct answers.""" + score = 0 + total = len(questions) + + print("\n" + "=" * 50) + print(" PYTHON KNOWLEDGE QUIZ") + print("=" * 50) + + for i, q in enumerate(questions, start=1): + print(f"\nQuestion {i}/{total}: {q['question']}") + for option in q["options"]: + print(f" {option}") + + while True: + answer = input("\nYour answer (A/B/C): ").strip().upper() + if answer in ("A", "B", "C"): + break + print("Please enter A, B, or C.") + + if answer == q["answer"]: + print("Correct!") + score += 1 + else: + print(f"Wrong. The answer was {q['answer']}.") + + return score + +def main() -> None: + score = run_quiz(QUESTIONS) + total = len(QUESTIONS) + percentage = (score / total) * 100 + + print("\n" + "=" * 50) + print(f" QUIZ COMPLETE! Score: {score}/{total} ({percentage:.0f}%)") + if percentage == 100: + print(" Perfect score! You are a genius!") + elif percentage >= 60: + print(" Good work! Keep learning!") + else: + print(" Keep studying - you will get there!") + print("=" * 50) + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain: + +1. What is the main concept covered today? +2. Write a short example from memory. +3. What is one common mistake with this concept? +4. How will you use this in real projects? + +--- diff --git a/src/content/day-05/solution.py b/src/content/day-05/solution.py new file mode 100644 index 0000000..b6e4f56 --- /dev/null +++ b/src/content/day-05/solution.py @@ -0,0 +1,82 @@ +""" +Day 05 Project: Interactive Quiz +================================== +A multiple-choice quiz with scoring. +""" + +QUESTIONS = [ + { + "question": "What does CPU stand for?", + "options": ["A) Central Processing Unit", "B) Computer Personal Unit", "C) Core Processing Utility"], + "answer": "A", + }, + { + "question": "How many bits are in a byte?", + "options": ["A) 4", "B) 8", "C) 16"], + "answer": "B", + }, + { + "question": "Which language was Python named after?", + "options": ["A) A snake", "B) A Greek god", "C) Monty Python (the comedy group)"], + "answer": "C", + }, + { + "question": "What does HTML stand for?", + "options": ["A) HyperText Markup Language", "B) High Transfer Markup Layer", "C) HyperText Main Language"], + "answer": "A", + }, + { + "question": "Which symbol is used for comments in Python?", + "options": ["A) //", "B) /*", "C) #"], + "answer": "C", + }, +] + + +def run_quiz(questions: list[dict]) -> int: + """Run through questions and return the number of correct answers.""" + score = 0 + total = len(questions) + + print("\n" + "=" * 50) + print(" PYTHON KNOWLEDGE QUIZ") + print("=" * 50) + + for i, q in enumerate(questions, start=1): + print(f"\nQuestion {i}/{total}: {q['question']}") + for option in q["options"]: + print(f" {option}") + + while True: + answer = input("\nYour answer (A/B/C): ").strip().upper() + if answer in ("A", "B", "C"): + break + print("Please enter A, B, or C.") + + if answer == q["answer"]: + print("Correct!") + score += 1 + else: + print(f"Wrong. The answer was {q['answer']}.") + + return score + + +def main() -> None: + score = run_quiz(QUESTIONS) + total = len(QUESTIONS) + percentage = (score / total) * 100 + + print("\n" + "=" * 50) + print(f" QUIZ COMPLETE! Score: {score}/{total} ({percentage:.0f}%)") + if percentage == 100: + print(" Perfect score! You are a genius!") + elif percentage >= 60: + print(" Good work! Keep learning!") + else: + print(" Keep studying - you will get there!") + print("=" * 50) + + +if __name__ == "__main__": + main() diff --git a/src/content/day-06/lesson.md b/src/content/day-06/lesson.md new file mode 100644 index 0000000..4e0da7f --- /dev/null +++ b/src/content/day-06/lesson.md @@ -0,0 +1,175 @@ +# Day 06 📋 - Lists + +--- + +## Overview + +Lists are Python's most versatile data structure. Learn to create, modify, and iterate over ordered collections of items. + +**What you will learn today:** + +- Creating lists with `[]` +- Indexing and slicing lists +- Adding items: `append()`, `insert()`, `extend()` +- Removing items: `remove()`, `pop()`, `del` +- Sorting with `sort()` and `sorted()` +- Common list methods and patterns + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `list` | An ordered, mutable sequence that can hold items of any type, including other lists. | +| `append()` | Adds one item to the end of a list. Modifies the list in place. | +| `pop()` | Removes and returns an item by index. Without an index, removes the last item. | +| `sorted()` | Returns a new sorted list without modifying the original. Use `sort()` to sort in place. | +| `len()` | Returns the number of items in a list. | + +--- + +## Code Examples + +### Creating and accessing lists + +```python +# Create a list with square brackets +fruits = ["apple", "banana", "cherry", "date"] + +# Access by index (0-based) +print(fruits[0]) # apple +print(fruits[-1]) # date (last item) +print(fruits[1:3]) # ['banana', 'cherry'] (slice) + +# Check if an item is in the list +print("banana" in fruits) # True +print("mango" in fruits) # False + +# Get the length +print(len(fruits)) # 4 +``` + +### Modifying lists + +```python +numbers = [3, 1, 4, 1, 5, 9, 2, 6] + +# Add items +numbers.append(5) # Add to end: [3,1,4,1,5,9,2,6,5] +numbers.insert(0, 0) # Insert at index 0: [0,3,1,4,...] +numbers.extend([7, 8]) # Add multiple items from another list + +# Remove items +numbers.remove(1) # Removes FIRST occurrence of 1 +popped = numbers.pop() # Remove and return last item +del numbers[0] # Delete item at index 0 + +# Sort +numbers.sort() # Sort in place (ascending) +numbers.sort(reverse=True) # Sort descending +new_sorted = sorted(numbers) # Returns new sorted list +``` + +### Iterating and transforming + +```python +colors = ["red", "green", "blue", "yellow"] + +# Iterate with a for loop +for color in colors: + print(color.upper()) + +# Iterate with index using enumerate() +for i, color in enumerate(colors): + print(f"{i}: {color}") + +# List comprehension (preview of Day 17) +lengths = [len(color) for color in colors] +print(lengths) # [3, 5, 4, 6] +``` + +--- + +## Today's Project: Shopping List Manager + +> Build an interactive shopping list where you can add items, remove items, and view your list - all from the terminal. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 06 Project: Shopping List Manager +======================================= +Manage a shopping list with add, remove, and view commands. +""" + +def display_list(items: list[str]) -> None: + """Print the current shopping list.""" + print("\n--- Shopping List ---") + if not items: + print(" (empty)") + else: + for i, item in enumerate(items, start=1): + print(f" {i}. {item}") + print("--------------------") + +def main() -> None: + shopping_list: list[str] = [] + + print("=" * 40) + print(" SHOPPING LIST MANAGER") + print("=" * 40) + print("Commands: add | remove | view | clear | quit") + + while True: + command = input("\n> ").strip().lower() + + if command == "add": + item = input(" Item to add: ").strip() + if item: + shopping_list.append(item.title()) + print(f" Added: {item.title()}") + else: + print(" Item name cannot be empty.") + + elif command == "remove": + display_list(shopping_list) + if shopping_list: + try: + idx = int(input(" Enter item number to remove: ")) - 1 + removed = shopping_list.pop(idx) + print(f" Removed: {removed}") + except (ValueError, IndexError): + print(" Invalid number.") + + elif command == "view": + display_list(shopping_list) + + elif command == "clear": + shopping_list.clear() + print(" List cleared.") + + elif command == "quit": + print(f"\nFinal list has {len(shopping_list)} item(s). Goodbye!") + break + + else: + print(" Unknown command. Try: add, remove, view, clear, quit") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain: + +1. What is the main concept covered today? +2. Write a short example from memory. +3. What is one common mistake with this concept? +4. How will you use this in real projects? + +--- diff --git a/src/content/day-06/solution.py b/src/content/day-06/solution.py new file mode 100644 index 0000000..3fe3375 --- /dev/null +++ b/src/content/day-06/solution.py @@ -0,0 +1,64 @@ +""" +Day 06 Project: Shopping List Manager +======================================= +Manage a shopping list with add, remove, and view commands. +""" + + +def display_list(items: list[str]) -> None: + """Print the current shopping list.""" + print("\n--- Shopping List ---") + if not items: + print(" (empty)") + else: + for i, item in enumerate(items, start=1): + print(f" {i}. {item}") + print("--------------------") + + +def main() -> None: + shopping_list: list[str] = [] + + print("=" * 40) + print(" SHOPPING LIST MANAGER") + print("=" * 40) + print("Commands: add | remove | view | clear | quit") + + while True: + command = input("\n> ").strip().lower() + + if command == "add": + item = input(" Item to add: ").strip() + if item: + shopping_list.append(item.title()) + print(f" Added: {item.title()}") + else: + print(" Item name cannot be empty.") + + elif command == "remove": + display_list(shopping_list) + if shopping_list: + try: + idx = int(input(" Enter item number to remove: ")) - 1 + removed = shopping_list.pop(idx) + print(f" Removed: {removed}") + except (ValueError, IndexError): + print(" Invalid number.") + + elif command == "view": + display_list(shopping_list) + + elif command == "clear": + shopping_list.clear() + print(" List cleared.") + + elif command == "quit": + print(f"\nFinal list has {len(shopping_list)} item(s). Goodbye!") + break + + else: + print(" Unknown command. Try: add, remove, view, clear, quit") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-07/lesson.md b/src/content/day-07/lesson.md new file mode 100644 index 0000000..f799ebe --- /dev/null +++ b/src/content/day-07/lesson.md @@ -0,0 +1,181 @@ +# Day 07 🎯 - Tuples, Sets & Booleans + +--- + +## Overview + +Understand when to use tuples (immutable sequences), sets (unique collections), and how Boolean logic powers decisions in your code. + +**What you will learn today:** + +- Tuples: immutable sequences +- When to use tuples vs lists +- Sets: unordered unique collections +- Set operations: union, intersection, difference +- Boolean operators: `and`, `or`, `not` +- Truthiness and falsy values + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `tuple` | An immutable sequence. Use when data should not change: coordinates, RGB values, database records. | +| `set` | An unordered collection of unique items. Adding a duplicate has no effect. | +| `frozenset` | An immutable version of a set. | +| `Truthy/Falsy` | In Python, `0`, `''`, `[]`, `{}`, `None` are all falsy. Everything else is truthy. | + +--- + +## Code Examples + +### Tuples + +```python +# Create with parentheses (or just commas) +point = (10, 20) +rgb = (255, 128, 0) +single = (42,) # Note the trailing comma for single-item tuples + +# Unpack tuples +x, y = point +print(f"x={x}, y={y}") # x=10, y=20 + +# Tuples are immutable +# point[0] = 5 # TypeError: 'tuple' object does not support item assignment + +# Functions can return multiple values as a tuple +def min_max(numbers): + return min(numbers), max(numbers) + +low, high = min_max([3, 1, 4, 1, 5, 9]) +print(low, high) # 1 9 +``` + +### Sets + +```python +# Create a set with curly braces +unique_colors = {"red", "green", "blue", "red", "green"} +print(unique_colors) # {'blue', 'green', 'red'} - duplicates removed + +# Set operations +a = {1, 2, 3, 4, 5} +b = {4, 5, 6, 7, 8} + +print(a | b) # Union: {1,2,3,4,5,6,7,8} +print(a & b) # Intersection: {4, 5} +print(a - b) # Difference: {1, 2, 3} +print(a ^ b) # Symmetric diff: {1,2,3,6,7,8} + +# Fast membership testing +words = {"hello", "world", "python"} +print("python" in words) # True - O(1) average time +``` + +### Boolean logic + +```python +# Boolean operators +print(True and False) # False +print(True or False) # True +print(not True) # False + +# Short-circuit evaluation +x = None +name = x or "default" # "default" (x is falsy) +print(name) + +# Falsy values: 0, 0.0, "", [], {}, set(), None, False +values = [0, "", [], None, False, 42, "hi", [1]] +for v in values: + status = "truthy" if v else "falsy" + print(f"{str(v):<10} is {status}") +``` + +--- + +## Today's Project: Unique Word Counter + +> Read a block of text from the user and report unique word count, most common words, and set operations on two texts. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 07 Project: Unique Word Counter +===================================== +Analyze text for unique words using sets. +""" +import string + +def clean_words(text: str) -> set[str]: + """Extract a set of unique lowercase words from text.""" + # Remove punctuation and split into words + translator = str.maketrans("", "", string.punctuation) + cleaned = text.lower().translate(translator) + return set(cleaned.split()) + +def word_frequency(text: str) -> dict[str, int]: + """Count how often each word appears.""" + translator = str.maketrans("", "", string.punctuation) + words = text.lower().translate(translator).split() + freq: dict[str, int] = {} + for word in words: + freq[word] = freq.get(word, 0) + 1 + return freq + +def main() -> None: + print("=" * 50) + print(" UNIQUE WORD COUNTER") + print("=" * 50) + + print("\nPaste Text 1 (press Enter twice when done):") + lines1 = [] + while True: + line = input() + if line == "": + break + lines1.append(line) + text1 = " ".join(lines1) + + print("\nPaste Text 2 (press Enter twice when done):") + lines2 = [] + while True: + line = input() + if line == "": + break + lines2.append(line) + text2 = " ".join(lines2) + + words1 = clean_words(text1) + words2 = clean_words(text2) + freq1 = word_frequency(text1) + + print(f"\n--- Text 1 Analysis ---") + print(f"Total unique words : {len(words1)}") + top5 = sorted(freq1, key=freq1.get, reverse=True)[:5] + print(f"Top 5 words : {top5}") + + print(f"\n--- Comparison ---") + print(f"Words in both : {words1 & words2}") + print(f"Only in Text 1 : {words1 - words2}") + print(f"Only in Text 2 : {words2 - words1}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain: + +1. What is the main concept covered today? +2. Write a short example from memory. +3. What is one common mistake with this concept? +4. How will you use this in real projects? + +--- diff --git a/src/content/day-07/solution.py b/src/content/day-07/solution.py new file mode 100644 index 0000000..39859d4 --- /dev/null +++ b/src/content/day-07/solution.py @@ -0,0 +1,66 @@ +""" +Day 07 Project: Unique Word Counter +===================================== +Analyze text for unique words using sets. +""" +import string + + +def clean_words(text: str) -> set[str]: + """Extract a set of unique lowercase words from text.""" + # Remove punctuation and split into words + translator = str.maketrans("", "", string.punctuation) + cleaned = text.lower().translate(translator) + return set(cleaned.split()) + + +def word_frequency(text: str) -> dict[str, int]: + """Count how often each word appears.""" + translator = str.maketrans("", "", string.punctuation) + words = text.lower().translate(translator).split() + freq: dict[str, int] = {} + for word in words: + freq[word] = freq.get(word, 0) + 1 + return freq + + +def main() -> None: + print("=" * 50) + print(" UNIQUE WORD COUNTER") + print("=" * 50) + + print("\nPaste Text 1 (press Enter twice when done):") + lines1 = [] + while True: + line = input() + if line == "": + break + lines1.append(line) + text1 = " ".join(lines1) + + print("\nPaste Text 2 (press Enter twice when done):") + lines2 = [] + while True: + line = input() + if line == "": + break + lines2.append(line) + text2 = " ".join(lines2) + + words1 = clean_words(text1) + words2 = clean_words(text2) + freq1 = word_frequency(text1) + + print(f"\n--- Text 1 Analysis ---") + print(f"Total unique words : {len(words1)}") + top5 = sorted(freq1, key=freq1.get, reverse=True)[:5] + print(f"Top 5 words : {top5}") + + print(f"\n--- Comparison ---") + print(f"Words in both : {words1 & words2}") + print(f"Only in Text 1 : {words1 - words2}") + print(f"Only in Text 2 : {words2 - words1}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-08/lesson.md b/src/content/day-08/lesson.md new file mode 100644 index 0000000..45e2c8d --- /dev/null +++ b/src/content/day-08/lesson.md @@ -0,0 +1,187 @@ +# Day 08 📖 - Dictionaries + +--- + +## Overview + +Dictionaries are Python's most powerful built-in data structure for mapping keys to values. Master them and you can model almost any real-world data. + +**What you will learn today:** + +- Creating dictionaries with `{}` +- Accessing values with keys and `.get()` +- Adding, updating, and deleting entries +- Iterating: `.keys()`, `.values()`, `.items()` +- Nested dictionaries +- Dictionary comprehensions + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `dict` | A mutable, unordered (Python 3.7+ maintains insertion order) collection of key-value pairs. | +| `.get(key, default)` | Safe key access. Returns `None` (or your default) if the key does not exist. Does not raise an error. | +| `.items()` | Returns view of `(key, value)` pairs - the most useful method for iterating over a dict. | +| `dict comprehension` | Build dictionaries with `{k: v for k, v in iterable}`. Elegant and fast. | + +--- + +## Code Examples + +### Creating and accessing dicts + +```python +# Create a dictionary +person = { + "name": "Alice", + "age": 28, + "city": "San Francisco", + "hobbies": ["coding", "reading"], +} + +# Access by key +print(person["name"]) # Alice +print(person.get("age")) # 28 +print(person.get("email")) # None (no error!) +print(person.get("email", "N/A")) # "N/A" (custom default) + +# Add and update +person["email"] = "alice@example.com" # Add new key +person["age"] = 29 # Update existing key + +# Delete +del person["city"] +removed = person.pop("hobbies") # Remove and return +``` + +### Iterating over dictionaries + +```python +scores = {"Alice": 95, "Bob": 82, "Charlie": 91} + +# Iterate over keys (default) +for name in scores: + print(name) + +# Iterate over values +for score in scores.values(): + print(score) + +# Iterate over key-value pairs (most common pattern) +for name, score in scores.items(): + print(f"{name}: {score}") + +# Find who has the highest score +top_student = max(scores, key=scores.get) +print(f"Top student: {top_student} ({scores[top_student]})") +``` + +### Nested dictionaries + +```python +users = { + "alice": {"email": "alice@ex.com", "role": "admin"}, + "bob": {"email": "bob@ex.com", "role": "user"}, +} + +# Access nested values +print(users["alice"]["role"]) # admin +print(users.get("charlie", {})) # {} - safe default + +# Iterate nested +for username, info in users.items(): + print(f"{username} ({info['role']}): {info['email']}") +``` + +--- + +## Today's Project: Contact Book + +> Build a contact book that stores names, phone numbers, and emails, with the ability to add, search, update, and delete contacts. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 08 Project: Contact Book +============================== +A contact manager using dictionaries. +""" + +def display_contact(name: str, info: dict) -> None: + """Pretty-print a single contact.""" + print(f"\n Name : {name}") + print(f" Phone : {info.get('phone', 'N/A')}") + print(f" Email : {info.get('email', 'N/A')}") + +def main() -> None: + contacts: dict[str, dict] = {} + + print("=" * 40) + print(" CONTACT BOOK") + print("=" * 40) + print("Commands: add | search | list | delete | quit") + + while True: + command = input("\n> ").strip().lower() + + if command == "add": + name = input(" Name : ").strip().title() + if not name: + print(" Name cannot be empty.") + continue + phone = input(" Phone : ").strip() + email = input(" Email : ").strip() + contacts[name] = {"phone": phone, "email": email} + print(f" Contact '{name}' saved.") + + elif command == "search": + query = input(" Search name: ").strip().title() + matches = {k: v for k, v in contacts.items() if query in k} + if matches: + for name, info in matches.items(): + display_contact(name, info) + else: + print(f" No contacts matching '{query}'.") + + elif command == "list": + if not contacts: + print(" No contacts saved yet.") + else: + print(f"\n {len(contacts)} contact(s):") + for name, info in sorted(contacts.items()): + display_contact(name, info) + + elif command == "delete": + name = input(" Name to delete: ").strip().title() + if name in contacts: + del contacts[name] + print(f" Deleted: {name}") + else: + print(f" Contact '{name}' not found.") + + elif command == "quit": + print("\nGoodbye!") + break + + else: + print(" Unknown command.") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain: + +1. What is the main concept covered today? +2. Write a short example from memory. +3. What is one common mistake with this concept? +4. How will you use this in real projects? + +--- diff --git a/src/content/day-08/solution.py b/src/content/day-08/solution.py new file mode 100644 index 0000000..7e841a9 --- /dev/null +++ b/src/content/day-08/solution.py @@ -0,0 +1,70 @@ +""" +Day 08 Project: Contact Book +============================== +A contact manager using dictionaries. +""" + + +def display_contact(name: str, info: dict) -> None: + """Pretty-print a single contact.""" + print(f"\n Name : {name}") + print(f" Phone : {info.get('phone', 'N/A')}") + print(f" Email : {info.get('email', 'N/A')}") + + +def main() -> None: + contacts: dict[str, dict] = {} + + print("=" * 40) + print(" CONTACT BOOK") + print("=" * 40) + print("Commands: add | search | list | delete | quit") + + while True: + command = input("\n> ").strip().lower() + + if command == "add": + name = input(" Name : ").strip().title() + if not name: + print(" Name cannot be empty.") + continue + phone = input(" Phone : ").strip() + email = input(" Email : ").strip() + contacts[name] = {"phone": phone, "email": email} + print(f" Contact '{name}' saved.") + + elif command == "search": + query = input(" Search name: ").strip().title() + matches = {k: v for k, v in contacts.items() if query in k} + if matches: + for name, info in matches.items(): + display_contact(name, info) + else: + print(f" No contacts matching '{query}'.") + + elif command == "list": + if not contacts: + print(" No contacts saved yet.") + else: + print(f"\n {len(contacts)} contact(s):") + for name, info in sorted(contacts.items()): + display_contact(name, info) + + elif command == "delete": + name = input(" Name to delete: ").strip().title() + if name in contacts: + del contacts[name] + print(f" Deleted: {name}") + else: + print(f" Contact '{name}' not found.") + + elif command == "quit": + print("\nGoodbye!") + break + + else: + print(" Unknown command.") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-09/lesson.md b/src/content/day-09/lesson.md new file mode 100644 index 0000000..29bc8ff --- /dev/null +++ b/src/content/day-09/lesson.md @@ -0,0 +1,157 @@ +# Day 09 🔀 - Conditionals + +--- + +## Overview + +Write programs that make decisions. `if`, `elif`, and `else` are the backbone of every program that responds differently to different inputs. + +**What you will learn today:** + +- `if`, `elif`, `else` syntax +- Comparison operators: `==`, `!=`, `<`, `>`, `<=`, `>=` +- Chaining conditions with `and`, `or`, `not` +- The ternary (one-line) operator +- Nested conditionals +- Python 3.10+ `match` statement + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `if / elif / else` | Execute different blocks of code based on conditions. Python uses indentation (4 spaces) to define blocks. | +| `Comparison operators` | `==` checks equality. `=` is assignment. One of the most common beginner mistakes is mixing them up. | +| `Ternary operator` | One-line conditional: `value_if_true if condition else value_if_false`. | +| `match statement` | Python 3.10+ structural pattern matching. Cleaner than long `elif` chains for matching values. | + +--- + +## Code Examples + +### if / elif / else + +```python +temperature = 22 + +if temperature < 0: + print("Freezing! Stay inside.") +elif temperature < 10: + print("Cold. Wear a coat.") +elif temperature < 20: + print("Cool. A light jacket works.") +elif temperature < 30: + print("Comfortable. Enjoy the weather!") +else: + print("Hot! Stay hydrated.") + +# Multiple conditions with and/or +age = 25 +has_id = True + +if age >= 18 and has_id: + print("Entry allowed.") +elif age >= 18 and not has_id: + print("Please bring your ID next time.") +else: + print("Entry denied. Must be 18+.") +``` + +### Ternary and match + +```python +# Ternary operator (one-line if/else) +score = 75 +grade = "Pass" if score >= 60 else "Fail" +print(grade) # Pass + +# Longer example +age = 20 +status = "adult" if age >= 18 else "minor" + +# match statement (Python 3.10+) +command = "quit" +match command: + case "start": + print("Starting...") + case "stop" | "quit": + print("Stopping.") + case _: + print(f"Unknown command: {command}") +``` + +--- + +## Today's Project: BMI Calculator + +> Calculate a user's Body Mass Index from their weight and height, then classify the result and provide context. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 09 Project: BMI Calculator +================================ +Calculate and classify BMI from weight and height. +""" + +def calculate_bmi(weight_kg: float, height_m: float) -> float: + """Calculate BMI. Raises ValueError for invalid inputs.""" + if weight_kg <= 0 or height_m <= 0: + raise ValueError("Weight and height must be positive numbers.") + return weight_kg / (height_m ** 2) + +def classify_bmi(bmi: float) -> tuple[str, str]: + """Return the BMI category and advice.""" + if bmi < 18.5: + return "Underweight", "Consider consulting a nutritionist." + elif bmi < 25: + return "Normal weight", "Great! Maintain your healthy lifestyle." + elif bmi < 30: + return "Overweight", "Regular exercise and a balanced diet can help." + else: + return "Obese", "Please consult a healthcare professional." + +def main() -> None: + print("=" * 45) + print(" BMI CALCULATOR") + print("=" * 45) + + try: + weight = float(input("\nEnter your weight (kg): ")) + height_cm = float(input("Enter your height (cm): ")) + height_m = height_cm / 100 + + bmi = calculate_bmi(weight, height_m) + category, advice = classify_bmi(bmi) + + print(f"\n BMI : {bmi:.1f}") + print(f" Category : {category}") + print(f" Advice : {advice}") + + # Visual BMI bar + bar_pos = min(max(int((bmi - 10) / 30 * 30), 0), 30) + bar = "-" * bar_pos + "^" + "-" * (30 - bar_pos) + print(f"\n [Underweight|Normal|Overweight|Obese]") + print(f" [{bar}]") + + except ValueError as e: + print(f"\nError: {e}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain: + +1. What is the main concept covered today? +2. Write a short example from memory. +3. What is one common mistake with this concept? +4. How will you use this in real projects? + +--- diff --git a/src/content/day-09/solution.py b/src/content/day-09/solution.py new file mode 100644 index 0000000..18c6afd --- /dev/null +++ b/src/content/day-09/solution.py @@ -0,0 +1,55 @@ +""" +Day 09 Project: BMI Calculator +================================ +Calculate and classify BMI from weight and height. +""" + + +def calculate_bmi(weight_kg: float, height_m: float) -> float: + """Calculate BMI. Raises ValueError for invalid inputs.""" + if weight_kg <= 0 or height_m <= 0: + raise ValueError("Weight and height must be positive numbers.") + return weight_kg / (height_m ** 2) + + +def classify_bmi(bmi: float) -> tuple[str, str]: + """Return the BMI category and advice.""" + if bmi < 18.5: + return "Underweight", "Consider consulting a nutritionist." + elif bmi < 25: + return "Normal weight", "Great! Maintain your healthy lifestyle." + elif bmi < 30: + return "Overweight", "Regular exercise and a balanced diet can help." + else: + return "Obese", "Please consult a healthcare professional." + + +def main() -> None: + print("=" * 45) + print(" BMI CALCULATOR") + print("=" * 45) + + try: + weight = float(input("\nEnter your weight (kg): ")) + height_cm = float(input("Enter your height (cm): ")) + height_m = height_cm / 100 + + bmi = calculate_bmi(weight, height_m) + category, advice = classify_bmi(bmi) + + print(f"\n BMI : {bmi:.1f}") + print(f" Category : {category}") + print(f" Advice : {advice}") + + # Visual BMI bar + bar_pos = min(max(int((bmi - 10) / 30 * 30), 0), 30) + bar = "-" * bar_pos + "^" + "-" * (30 - bar_pos) + print(f"\n [Underweight|Normal|Overweight|Obese]") + print(f" [{bar}]") + + except ValueError as e: + print(f"\nError: {e}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-10/lesson.md b/src/content/day-10/lesson.md new file mode 100644 index 0000000..d6b23d8 --- /dev/null +++ b/src/content/day-10/lesson.md @@ -0,0 +1,169 @@ +# Day 10 🔄 - for Loops + +--- + +## Overview + +The `for` loop is Python's workhorse for iteration. Learn to loop over any sequence, use `enumerate()` and `zip()`, and understand `range()`. + +**What you will learn today:** + +- `for` loop syntax and how it works +- Looping with `range(start, stop, step)` +- `enumerate()` for index + value pairs +- `zip()` to loop multiple sequences together +- `break` and `continue` for loop control +- Nested loops and their use cases + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `for` | Iterates over any iterable: list, string, range, tuple, dict, file, and more. | +| `range()` | Generates a sequence of numbers. `range(5)` gives 0,1,2,3,4. `range(1,10,2)` gives 1,3,5,7,9. | +| `enumerate()` | Returns `(index, value)` pairs. Avoids the need for a manual counter variable. | +| `zip()` | Pairs elements from multiple iterables into tuples. Stops at the shortest one. | +| `break / continue` | `break` exits the loop entirely. `continue` skips the rest of the current iteration. | + +--- + +## Code Examples + +### range() and basic for loops + +```python +# Loop a fixed number of times +for i in range(5): + print(i) # 0 1 2 3 4 + +# Loop with start and stop +for i in range(1, 6): + print(i) # 1 2 3 4 5 + +# Loop with step +for i in range(0, 20, 5): + print(i) # 0 5 10 15 + +# Countdown +for i in range(10, 0, -1): + print(i, end=" ") # 10 9 8 7 6 5 4 3 2 1 + +# Loop over a list +fruits = ["apple", "banana", "cherry"] +for fruit in fruits: + print(fruit.upper()) +``` + +### enumerate() and zip() + +```python +students = ["Alice", "Bob", "Charlie"] +scores = [95, 82, 91] + +# enumerate: get index and value together +for i, student in enumerate(students, start=1): + print(f"{i}. {student}") +# 1. Alice +# 2. Bob +# 3. Charlie + +# zip: iterate two lists at the same time +for student, score in zip(students, scores): + print(f"{student}: {score}") +# Alice: 95 +# Bob: 82 + +# break and continue +for i in range(10): + if i == 3: + continue # skip 3 + if i == 7: + break # stop at 7 + print(i, end=" ") # 0 1 2 4 5 6 +``` + +--- + +## Today's Project: Multiplication Table + +> Generate a formatted multiplication table for any number and size the user requests. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 10 Project: Multiplication Table +====================================== +Generate formatted multiplication tables. +""" + +def print_multiplication_table(number: int, size: int = 10) -> None: + """Print a multiplication table for the given number.""" + print(f"\n Multiplication table for {number} (up to {size})") + print(" " + "-" * 30) + for i in range(1, size + 1): + result = number * i + print(f" {number:>3} x {i:>3} = {result:>5}") + print(" " + "-" * 30) + +def print_full_grid(size: int = 10) -> None: + """Print a full N x N multiplication grid.""" + print(f"\n Full {size} x {size} Multiplication Grid") + print() + + # Header row + header = " " + "".join(f"{i:>5}" for i in range(1, size + 1)) + print(header) + print(" " + "-" * (size * 5)) + + for row in range(1, size + 1): + line = f"{row:>3} |" + for col in range(1, size + 1): + line += f"{row * col:>5}" + print(line) + +def main() -> None: + print("=" * 45) + print(" MULTIPLICATION TABLE GENERATOR") + print("=" * 45) + print("\nChoose mode:") + print(" 1. Table for one number") + print(" 2. Full multiplication grid") + + choice = input("\nYour choice (1/2): ").strip() + + if choice == "1": + try: + number = int(input("Enter the number: ")) + size = int(input("Table size (default 10): ") or "10") + print_multiplication_table(number, size) + except ValueError: + print("Please enter valid integers.") + + elif choice == "2": + try: + size = int(input("Grid size (default 10): ") or "10") + print_full_grid(size) + except ValueError: + print("Please enter a valid integer.") + else: + print("Invalid choice.") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain: + +1. What is the main concept covered today? +2. Write a short example from memory without looking at the lesson. +3. What is one common mistake with this concept, and how do you avoid it? +4. How will you use this in real projects? + +--- diff --git a/src/content/day-10/solution.py b/src/content/day-10/solution.py new file mode 100644 index 0000000..8eb3c18 --- /dev/null +++ b/src/content/day-10/solution.py @@ -0,0 +1,64 @@ +""" +Day 10 Project: Multiplication Table +====================================== +Generate formatted multiplication tables. +""" + + +def print_multiplication_table(number: int, size: int = 10) -> None: + """Print a multiplication table for the given number.""" + print(f"\n Multiplication table for {number} (up to {size})") + print(" " + "-" * 30) + for i in range(1, size + 1): + result = number * i + print(f" {number:>3} x {i:>3} = {result:>5}") + print(" " + "-" * 30) + + +def print_full_grid(size: int = 10) -> None: + """Print a full N x N multiplication grid.""" + print(f"\n Full {size} x {size} Multiplication Grid") + print() + + # Header row + header = " " + "".join(f"{i:>5}" for i in range(1, size + 1)) + print(header) + print(" " + "-" * (size * 5)) + + for row in range(1, size + 1): + line = f"{row:>3} |" + for col in range(1, size + 1): + line += f"{row * col:>5}" + print(line) + + +def main() -> None: + print("=" * 45) + print(" MULTIPLICATION TABLE GENERATOR") + print("=" * 45) + print("\nChoose mode:") + print(" 1. Table for one number") + print(" 2. Full multiplication grid") + + choice = input("\nYour choice (1/2): ").strip() + + if choice == "1": + try: + number = int(input("Enter the number: ")) + size = int(input("Table size (default 10): ") or "10") + print_multiplication_table(number, size) + except ValueError: + print("Please enter valid integers.") + + elif choice == "2": + try: + size = int(input("Grid size (default 10): ") or "10") + print_full_grid(size) + except ValueError: + print("Please enter a valid integer.") + else: + print("Invalid choice.") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-11/lesson.md b/src/content/day-11/lesson.md new file mode 100644 index 0000000..5215617 --- /dev/null +++ b/src/content/day-11/lesson.md @@ -0,0 +1,187 @@ +# Day 11 🔁 - while Loops + +--- + +## Overview + +The `while` loop runs as long as a condition is true. Learn when to use `while` over `for`, and how to build interactive programs with loop control. + +**What you will learn today:** + +- `while` loop syntax +- Avoiding infinite loops +- `break` to exit, `continue` to skip +- The `while...else` clause +- Sentinel values for loop termination +- Building interactive menus with `while True` + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `while` | Repeats a block of code as long as the condition is `True`. Check the condition before every iteration. | +| `Infinite loop` | A `while True:` loop runs forever unless a `break` is encountered. Useful for menus and event loops. | +| `while...else` | The `else` block after a `while` runs when the condition becomes `False`, but NOT if `break` was used. | +| `Sentinel value` | A special value that signals the loop should stop. For example, asking user to enter -1 to quit. | + +--- + +## Code Examples + +### Basic while loops + +```python +# Count up +count = 0 +while count < 5: + print(count) + count += 1 # IMPORTANT: must move toward termination! +# 0 1 2 3 4 + +# User-driven loop with sentinel +total = 0 +print("Enter numbers to sum. Enter 0 to stop.") +while True: + num = float(input("Number: ")) + if num == 0: + break + total += num +print(f"Total: {total}") +``` + +### while...else and menus + +```python +# while...else: else runs if loop ends naturally (no break) +n = 10 +while n > 0: + n -= 3 +else: + print("Loop ended naturally. n =", n) # n = -2 + +# Interactive menu pattern (extremely common in Python programs) +menu = """ +1. Say hello +2. Print date +3. Quit +""" +while True: + print(menu) + choice = input("Choose (1-3): ").strip() + if choice == "1": + print("Hello!") + elif choice == "2": + from datetime import date + print(date.today()) + elif choice == "3": + print("Goodbye!") + break + else: + print("Invalid choice. Try again.") +``` + +--- + +## Today's Project: Number Guessing Game + +> Build a number guessing game where the computer picks a random number and the player has limited attempts, with hot/cold hints. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 11 Project: Number Guessing Game +====================================== +Guess the number with hot/cold hints and limited attempts. +""" +import random + +def get_hint(guess: int, target: int) -> str: + """Return a directional and temperature hint.""" + diff = abs(guess - target) + direction = "higher" if target > guess else "lower" + + if diff == 0: + return "CORRECT!" + elif diff <= 3: + temperature = "Scorching hot" + elif diff <= 10: + temperature = "Very warm" + elif diff <= 20: + temperature = "Getting warm" + elif diff <= 40: + temperature = "Cold" + else: + temperature = "Freezing cold" + + return f"{temperature}. Go {direction}." + +def play_game(low: int = 1, high: int = 100, max_attempts: int = 7) -> bool: + """Run one round of the guessing game. Returns True if the player won.""" + target = random.randint(low, high) + print(f"\nI am thinking of a number between {low} and {high}.") + print(f"You have {max_attempts} attempts. Good luck!\n") + + for attempt in range(1, max_attempts + 1): + remaining = max_attempts - attempt + 1 + try: + guess = int(input(f"Attempt {attempt}/{max_attempts}: ")) + except ValueError: + print("Please enter a whole number.") + continue + + if guess < low or guess > high: + print(f"Please guess between {low} and {high}.") + continue + + hint = get_hint(guess, target) + print(f" Hint: {hint}") + + if guess == target: + print(f"\nYou got it in {attempt} attempt(s)!") + return True + + if remaining > 1: + print(f" ({remaining - 1} attempt(s) left)") + + print(f"\nGame over! The number was {target}.") + return False + +def main() -> None: + print("=" * 45) + print(" NUMBER GUESSING GAME") + print("=" * 45) + + wins = 0 + games = 0 + + while True: + won = play_game() + games += 1 + if won: + wins += 1 + + print(f"\nRecord: {wins}/{games} wins") + again = input("Play again? (y/n): ").strip().lower() + if again != "y": + print("\nThanks for playing!") + break + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain: + +1. What is the main concept covered today? +2. Write a short example from memory without looking at the lesson. +3. What is one common mistake with this concept, and how do you avoid it? +4. How will you use this in real projects? + +--- diff --git a/src/content/day-11/solution.py b/src/content/day-11/solution.py new file mode 100644 index 0000000..2648ee1 --- /dev/null +++ b/src/content/day-11/solution.py @@ -0,0 +1,84 @@ +""" +Day 11 Project: Number Guessing Game +====================================== +Guess the number with hot/cold hints and limited attempts. +""" +import random + + +def get_hint(guess: int, target: int) -> str: + """Return a directional and temperature hint.""" + diff = abs(guess - target) + direction = "higher" if target > guess else "lower" + + if diff == 0: + return "CORRECT!" + elif diff <= 3: + temperature = "Scorching hot" + elif diff <= 10: + temperature = "Very warm" + elif diff <= 20: + temperature = "Getting warm" + elif diff <= 40: + temperature = "Cold" + else: + temperature = "Freezing cold" + + return f"{temperature}. Go {direction}." + + +def play_game(low: int = 1, high: int = 100, max_attempts: int = 7) -> bool: + """Run one round of the guessing game. Returns True if the player won.""" + target = random.randint(low, high) + print(f"\nI am thinking of a number between {low} and {high}.") + print(f"You have {max_attempts} attempts. Good luck!\n") + + for attempt in range(1, max_attempts + 1): + remaining = max_attempts - attempt + 1 + try: + guess = int(input(f"Attempt {attempt}/{max_attempts}: ")) + except ValueError: + print("Please enter a whole number.") + continue + + if guess < low or guess > high: + print(f"Please guess between {low} and {high}.") + continue + + hint = get_hint(guess, target) + print(f" Hint: {hint}") + + if guess == target: + print(f"\nYou got it in {attempt} attempt(s)!") + return True + + if remaining > 1: + print(f" ({remaining - 1} attempt(s) left)") + + print(f"\nGame over! The number was {target}.") + return False + + +def main() -> None: + print("=" * 45) + print(" NUMBER GUESSING GAME") + print("=" * 45) + + wins = 0 + games = 0 + + while True: + won = play_game() + games += 1 + if won: + wins += 1 + + print(f"\nRecord: {wins}/{games} wins") + again = input("Play again? (y/n): ").strip().lower() + if again != "y": + print("\nThanks for playing!") + break + + +if __name__ == "__main__": + main() diff --git a/src/content/day-12/lesson.md b/src/content/day-12/lesson.md new file mode 100644 index 0000000..dbaaa64 --- /dev/null +++ b/src/content/day-12/lesson.md @@ -0,0 +1,201 @@ +# Day 12 ⚙️ - Functions Basics + +--- + +## Overview + +Functions let you write code once and reuse it everywhere. Learn to define functions, pass arguments, return values, and write proper docstrings. + +**What you will learn today:** + +- Defining functions with `def` +- Parameters vs arguments +- Return values and `return` +- Default parameter values +- Docstrings (PEP 257) +- The DRY principle (Don't Repeat Yourself) + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `def` | Defines a function. The function body runs only when the function is called. | +| `parameters` | Variables listed in the function definition: `def greet(name, greeting):` | +| `arguments` | The actual values passed when calling the function: `greet('Alice', 'Hello')`. | +| `return` | Sends a value back to the caller. A function without `return` returns `None`. | +| `docstring` | A string literal as the first statement of a function. Explains what the function does. | + +--- + +## Code Examples + +### Defining and calling functions + +```python +# Define once, call anywhere +def greet(name: str) -> str: + """Return a personalized greeting.""" + return f"Hello, {name}!" + +print(greet("Alice")) # Hello, Alice! +print(greet("Bob")) # Hello, Bob! + +# Default parameter values +def power(base: float, exp: float = 2) -> float: + """Return base raised to exp. Default is squaring.""" + return base ** exp + +print(power(3)) # 9.0 (uses default exp=2) +print(power(2, 10)) # 1024.0 +``` + +### Return values + +```python +# Functions can return any type +def is_even(n: int) -> bool: + """Return True if n is even.""" + return n % 2 == 0 + +print(is_even(4)) # True +print(is_even(7)) # False + +# Return multiple values (as a tuple) +def stats(numbers: list[float]) -> tuple[float, float, float]: + """Return the min, max, and average of a list.""" + return min(numbers), max(numbers), sum(numbers) / len(numbers) + +low, high, avg = stats([4, 7, 2, 9, 1]) +print(f"Min: {low}, Max: {high}, Avg: {avg:.1f}") + +# Early return +def safe_divide(a: float, b: float) -> float | None: + """Divide a by b, or return None if b is zero.""" + if b == 0: + return None # Early exit + return a / b +``` + +--- + +## Today's Project: Password Generator + +> Build a function-based password generator that creates secure random passwords based on user-specified length and character sets. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 12 Project: Password Generator +==================================== +Generate secure random passwords using functions. +""" +import random +import string + +def generate_password( + length: int = 16, + use_upper: bool = True, + use_lower: bool = True, + use_digits: bool = True, + use_symbols: bool = True, +) -> str: + """Generate a secure random password. + + Args: + length: Number of characters in the password (default 16). + use_upper: Include uppercase letters. + use_lower: Include lowercase letters. + use_digits: Include digits. + use_symbols: Include symbols. + + Returns: + A random password string. + + Raises: + ValueError: If no character sets are selected or length is too short. + """ + if length < 4: + raise ValueError("Password length must be at least 4.") + + charset = "" + guaranteed: list[str] = [] + + if use_upper: + charset += string.ascii_uppercase + guaranteed.append(random.choice(string.ascii_uppercase)) + if use_lower: + charset += string.ascii_lowercase + guaranteed.append(random.choice(string.ascii_lowercase)) + if use_digits: + charset += string.digits + guaranteed.append(random.choice(string.digits)) + if use_symbols: + charset += string.punctuation + guaranteed.append(random.choice(string.punctuation)) + + if not charset: + raise ValueError("Select at least one character type.") + + remaining_length = length - len(guaranteed) + rest = [random.choice(charset) for _ in range(remaining_length)] + all_chars = guaranteed + rest + random.shuffle(all_chars) + return "".join(all_chars) + +def rate_password(password: str) -> str: + """Rate the strength of a password.""" + score = 0 + if len(password) >= 12: + score += 1 + if any(c.isupper() for c in password): + score += 1 + if any(c.islower() for c in password): + score += 1 + if any(c.isdigit() for c in password): + score += 1 + if any(c in string.punctuation for c in password): + score += 1 + ratings = {5: "Very Strong", 4: "Strong", 3: "Moderate", 2: "Weak", 1: "Very Weak"} + return ratings.get(score, "Very Weak") + +def main() -> None: + print("=" * 45) + print(" PASSWORD GENERATOR") + print("=" * 45) + + try: + length = int(input("\nPassword length (default 16): ") or "16") + use_upper = input("Include uppercase? (Y/n): ").strip().lower() != "n" + use_lower = input("Include lowercase? (Y/n): ").strip().lower() != "n" + use_digits = input("Include digits? (Y/n): ").strip().lower() != "n" + use_symbols = input("Include symbols? (Y/n): ").strip().lower() != "n" + count = int(input("How many passwords? (default 5): ") or "5") + + print(f"\n{'Password':<{length + 4}} Strength") + print("-" * (length + 20)) + for _ in range(count): + pw = generate_password(length, use_upper, use_lower, use_digits, use_symbols) + strength = rate_password(pw) + print(f" {pw:<{length + 2}} {strength}") + except ValueError as e: + print(f"\nError: {e}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain: + +1. What is the main concept covered today? +2. Write a short example from memory without looking at the lesson. +3. What is one common mistake with this concept, and how do you avoid it? +4. How will you use this in real projects? + +--- diff --git a/src/content/day-12/solution.py b/src/content/day-12/solution.py new file mode 100644 index 0000000..a58675f --- /dev/null +++ b/src/content/day-12/solution.py @@ -0,0 +1,102 @@ +""" +Day 12 Project: Password Generator +==================================== +Generate secure random passwords using functions. +""" +import random +import string + + +def generate_password( + length: int = 16, + use_upper: bool = True, + use_lower: bool = True, + use_digits: bool = True, + use_symbols: bool = True, +) -> str: + """Generate a secure random password. + + Args: + length: Number of characters in the password (default 16). + use_upper: Include uppercase letters. + use_lower: Include lowercase letters. + use_digits: Include digits. + use_symbols: Include symbols. + + Returns: + A random password string. + + Raises: + ValueError: If no character sets are selected or length is too short. + """ + if length < 4: + raise ValueError("Password length must be at least 4.") + + charset = "" + guaranteed: list[str] = [] + + if use_upper: + charset += string.ascii_uppercase + guaranteed.append(random.choice(string.ascii_uppercase)) + if use_lower: + charset += string.ascii_lowercase + guaranteed.append(random.choice(string.ascii_lowercase)) + if use_digits: + charset += string.digits + guaranteed.append(random.choice(string.digits)) + if use_symbols: + charset += string.punctuation + guaranteed.append(random.choice(string.punctuation)) + + if not charset: + raise ValueError("Select at least one character type.") + + remaining_length = length - len(guaranteed) + rest = [random.choice(charset) for _ in range(remaining_length)] + all_chars = guaranteed + rest + random.shuffle(all_chars) + return "".join(all_chars) + + +def rate_password(password: str) -> str: + """Rate the strength of a password.""" + score = 0 + if len(password) >= 12: + score += 1 + if any(c.isupper() for c in password): + score += 1 + if any(c.islower() for c in password): + score += 1 + if any(c.isdigit() for c in password): + score += 1 + if any(c in string.punctuation for c in password): + score += 1 + ratings = {5: "Very Strong", 4: "Strong", 3: "Moderate", 2: "Weak", 1: "Very Weak"} + return ratings.get(score, "Very Weak") + + +def main() -> None: + print("=" * 45) + print(" PASSWORD GENERATOR") + print("=" * 45) + + try: + length = int(input("\nPassword length (default 16): ") or "16") + use_upper = input("Include uppercase? (Y/n): ").strip().lower() != "n" + use_lower = input("Include lowercase? (Y/n): ").strip().lower() != "n" + use_digits = input("Include digits? (Y/n): ").strip().lower() != "n" + use_symbols = input("Include symbols? (Y/n): ").strip().lower() != "n" + count = int(input("How many passwords? (default 5): ") or "5") + + print(f"\n{'Password':<{length + 4}} Strength") + print("-" * (length + 20)) + for _ in range(count): + pw = generate_password(length, use_upper, use_lower, use_digits, use_symbols) + strength = rate_password(pw) + print(f" {pw:<{length + 2}} {strength}") + except ValueError as e: + print(f"\nError: {e}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-13/lesson.md b/src/content/day-13/lesson.md new file mode 100644 index 0000000..86428c6 --- /dev/null +++ b/src/content/day-13/lesson.md @@ -0,0 +1,182 @@ +# Day 13 🎛️ - Function Arguments & Type Hints + +--- + +## Overview + +Go deeper into Python functions: positional, keyword, `*args`, `**kwargs`, type hints, and how to write functions that are both flexible and self-documenting. + +**What you will learn today:** + +- Positional vs keyword arguments +- `*args` for variable positional arguments +- `**kwargs` for variable keyword arguments +- Type hints with `->` and argument annotations +- The `typing` module for complex hints +- Argument ordering rules + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `*args` | Collects all extra positional arguments into a tuple. The `*` is the syntax; `args` is the conventional name. | +| `**kwargs` | Collects all extra keyword arguments into a dictionary. Use `**` syntax. | +| `Type hints` | Annotations that tell readers (and tools) what types a function expects and returns. Not enforced at runtime. | +| `Keyword-only args` | Place `*` in the parameter list to force subsequent arguments to be passed by keyword only. | + +--- + +## Code Examples + +### *args and **kwargs + +```python +# *args: collect extra positional arguments +def total(*args: float) -> float: + """Sum any number of values.""" + return sum(args) + +print(total(1, 2, 3)) # 6 +print(total(10, 20, 30, 40)) # 100 + +# **kwargs: collect extra keyword arguments +def describe(**kwargs: str) -> None: + """Print keyword descriptions.""" + for key, value in kwargs.items(): + print(f" {key}: {value}") + +describe(name="Alice", role="Developer", city="SF") + +# Combining all argument types +def configure(host: str, port: int = 8080, *flags: str, **options: str) -> None: + """Example of all argument types combined.""" + print(f"Host: {host}, Port: {port}") + print(f"Flags: {flags}") + print(f"Options: {options}") +``` + +### Type hints in practice + +```python +from typing import Optional, Union + +# Simple type hints +def add(a: int, b: int) -> int: + return a + b + +# Optional means the value can be None +def find_user(user_id: int) -> Optional[str]: + users = {1: "Alice", 2: "Bob"} + return users.get(user_id) + +# Union (Python 3.9: use X | Y syntax instead) +def process(value: int | str) -> str: + return str(value).upper() + +# List, dict generics (Python 3.9+) +def average(nums: list[float]) -> float: + return sum(nums) / len(nums) if nums else 0.0 + +def merge(a: dict[str, int], b: dict[str, int]) -> dict[str, int]: + return {**a, **b} +``` + +--- + +## Today's Project: Currency Converter + +> Build a currency converter using a dictionary of exchange rates, with support for listing all currencies and chaining conversions. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 13 Project: Currency Converter +==================================== +Convert between currencies using exchange rates. +""" + +# Exchange rates relative to USD +RATES: dict[str, float] = { + "USD": 1.00, + "EUR": 0.92, + "GBP": 0.79, + "JPY": 149.50, + "CAD": 1.36, + "AUD": 1.53, + "CHF": 0.89, + "CNY": 7.24, + "INR": 83.12, + "BRL": 4.97, +} + +def convert( + amount: float, + from_currency: str, + to_currency: str, + rates: dict[str, float] = RATES, +) -> float: + """Convert amount from one currency to another. + + Args: + amount: The amount to convert. + from_currency: ISO 4217 currency code (e.g. "USD"). + to_currency: Target ISO 4217 currency code. + rates: Exchange rate dictionary keyed by USD. + + Returns: + Converted amount as a float. + + Raises: + KeyError: If either currency code is not in rates. + """ + from_rate = rates[from_currency.upper()] + to_rate = rates[to_currency.upper()] + usd_amount = amount / from_rate + return usd_amount * to_rate + +def list_currencies(*_, rates: dict[str, float] = RATES) -> None: + """Print all available currencies.""" + print("\nAvailable currencies:") + for code in sorted(rates): + print(f" {code}") + +def main() -> None: + print("=" * 45) + print(" CURRENCY CONVERTER") + print("=" * 45) + print("Type 'list' to see available currencies, 'quit' to exit.\n") + + while True: + cmd = input("> ").strip().lower() + + if cmd == "quit": + break + elif cmd == "list": + list_currencies() + continue + + try: + amount = float(input(" Amount : ")) + from_c = input(" From : ").strip().upper() + to_c = input(" To : ").strip().upper() + result = convert(amount, from_c, to_c) + print(f"\n {amount:.2f} {from_c} = {result:.2f} {to_c}\n") + except KeyError as e: + print(f" Unknown currency: {e}. Type 'list' to see options.") + except ValueError: + print(" Please enter a valid number for the amount.") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-13/solution.py b/src/content/day-13/solution.py new file mode 100644 index 0000000..464187a --- /dev/null +++ b/src/content/day-13/solution.py @@ -0,0 +1,83 @@ +""" +Day 13 Project: Currency Converter +==================================== +Convert between currencies using exchange rates. +""" + +# Exchange rates relative to USD +RATES: dict[str, float] = { + "USD": 1.00, + "EUR": 0.92, + "GBP": 0.79, + "JPY": 149.50, + "CAD": 1.36, + "AUD": 1.53, + "CHF": 0.89, + "CNY": 7.24, + "INR": 83.12, + "BRL": 4.97, +} + + +def convert( + amount: float, + from_currency: str, + to_currency: str, + rates: dict[str, float] = RATES, +) -> float: + """Convert amount from one currency to another. + + Args: + amount: The amount to convert. + from_currency: ISO 4217 currency code (e.g. "USD"). + to_currency: Target ISO 4217 currency code. + rates: Exchange rate dictionary keyed by USD. + + Returns: + Converted amount as a float. + + Raises: + KeyError: If either currency code is not in rates. + """ + from_rate = rates[from_currency.upper()] + to_rate = rates[to_currency.upper()] + usd_amount = amount / from_rate + return usd_amount * to_rate + + +def list_currencies(*_, rates: dict[str, float] = RATES) -> None: + """Print all available currencies.""" + print("\nAvailable currencies:") + for code in sorted(rates): + print(f" {code}") + + +def main() -> None: + print("=" * 45) + print(" CURRENCY CONVERTER") + print("=" * 45) + print("Type 'list' to see available currencies, 'quit' to exit.\n") + + while True: + cmd = input("> ").strip().lower() + + if cmd == "quit": + break + elif cmd == "list": + list_currencies() + continue + + try: + amount = float(input(" Amount : ")) + from_c = input(" From : ").strip().upper() + to_c = input(" To : ").strip().upper() + result = convert(amount, from_c, to_c) + print(f"\n {amount:.2f} {from_c} = {result:.2f} {to_c}\n") + except KeyError as e: + print(f" Unknown currency: {e}. Type 'list' to see options.") + except ValueError: + print(" Please enter a valid number for the amount.") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-14/lesson.md b/src/content/day-14/lesson.md new file mode 100644 index 0000000..53ac4a2 --- /dev/null +++ b/src/content/day-14/lesson.md @@ -0,0 +1,177 @@ +# Day 14 🔭 - Scope & Closures + +--- + +## Overview + +Understanding scope tells you where a variable can be accessed. Closures take this further, letting inner functions remember their enclosing environment. + +**What you will learn today:** + +- The LEGB rule: Local, Enclosing, Global, Built-in +- `global` keyword to modify module-level variables +- `nonlocal` keyword to modify enclosing scope variables +- What a closure is and when to use one +- Practical closure patterns +- Why closures matter for decorators (preview) + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `LEGB rule` | Python looks up names in this order: Local scope, then Enclosing function scope, then Global (module) scope, then Built-in names. | +| `global` | Declares that a variable inside a function refers to the module-level name. Use sparingly. | +| `nonlocal` | Lets an inner function modify a variable in its enclosing (but not global) scope. | +| `closure` | A function that remembers variables from its enclosing scope even after the outer function has returned. | + +--- + +## Code Examples + +### LEGB and global + +```python +x = "global" + +def outer(): + x = "enclosing" + + def inner(): + x = "local" + print(x) # local (local scope wins) + + inner() + print(x) # enclosing + +outer() +print(x) # global + +# global keyword +count = 0 + +def increment(): + global count # modify the module-level count + count += 1 + +increment() +increment() +print(count) # 2 +``` + +### Closures + +```python +def make_counter(start: int = 0, step: int = 1): + """Return a counter function that remembers its state.""" + current = start + + def counter() -> int: + nonlocal current + value = current + current += step + return value + + return counter # Return the function, not the result! + +count_by_one = make_counter() +count_by_five = make_counter(step=5) + +print(count_by_one()) # 0 +print(count_by_one()) # 1 +print(count_by_one()) # 2 +print(count_by_five()) # 0 +print(count_by_five()) # 5 +# Each closure has its own independent state! +``` + +--- + +## Today's Project: Counter Factory + +> Build a closure-based counter factory that creates independent counters with configurable start, step, and max values. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 14 Project: Counter Factory +================================= +Closure-powered independent counters. +""" +from typing import Callable + +def make_counter( + start: int = 0, + step: int = 1, + max_value: int | None = None, + cycle: bool = False, +) -> Callable[[], int | None]: + """Create an independent counter function. + + Args: + start: Initial value (default 0). + step: Increment per call (default 1). + max_value: Upper bound. None means unlimited. + cycle: If True, resets to start when max_value is reached. + + Returns: + A callable that returns the next counter value, + or None if max_value was reached and cycle is False. + """ + current = start + + def counter() -> int | None: + nonlocal current + + if max_value is not None and current > max_value: + if cycle: + current = start + else: + return None + + value = current + current += step + return value + + return counter + +def main() -> None: + print("=" * 45) + print(" COUNTER FACTORY DEMO") + print("=" * 45) + + # Standard counter + c1 = make_counter(start=1) + print("\nCounter 1 (start=1, step=1):", [c1() for _ in range(5)]) + + # Step counter + c2 = make_counter(start=0, step=10) + print("Counter 2 (start=0, step=10):", [c2() for _ in range(5)]) + + # Cycling counter + c3 = make_counter(start=1, step=1, max_value=3, cycle=True) + print("Counter 3 (cycling 1-3):", [c3() for _ in range(9)]) + + # Bounded counter + c4 = make_counter(start=10, step=-2, max_value=10) + print("Counter 4 (countdown from 10):", end=" ") + results = [] + while (val := c4()) is not None: + results.append(val) + print(results) + + print("\nAll counters are independent - they never interfere with each other!") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-14/solution.py b/src/content/day-14/solution.py new file mode 100644 index 0000000..ff8246c --- /dev/null +++ b/src/content/day-14/solution.py @@ -0,0 +1,74 @@ +""" +Day 14 Project: Counter Factory +================================= +Closure-powered independent counters. +""" +from typing import Callable + + +def make_counter( + start: int = 0, + step: int = 1, + max_value: int | None = None, + cycle: bool = False, +) -> Callable[[], int | None]: + """Create an independent counter function. + + Args: + start: Initial value (default 0). + step: Increment per call (default 1). + max_value: Upper bound. None means unlimited. + cycle: If True, resets to start when max_value is reached. + + Returns: + A callable that returns the next counter value, + or None if max_value was reached and cycle is False. + """ + current = start + + def counter() -> int | None: + nonlocal current + + if max_value is not None and current > max_value: + if cycle: + current = start + else: + return None + + value = current + current += step + return value + + return counter + + +def main() -> None: + print("=" * 45) + print(" COUNTER FACTORY DEMO") + print("=" * 45) + + # Standard counter + c1 = make_counter(start=1) + print("\nCounter 1 (start=1, step=1):", [c1() for _ in range(5)]) + + # Step counter + c2 = make_counter(start=0, step=10) + print("Counter 2 (start=0, step=10):", [c2() for _ in range(5)]) + + # Cycling counter + c3 = make_counter(start=1, step=1, max_value=3, cycle=True) + print("Counter 3 (cycling 1-3):", [c3() for _ in range(9)]) + + # Bounded counter + c4 = make_counter(start=10, step=-2, max_value=10) + print("Counter 4 (countdown from 10):", end=" ") + results = [] + while (val := c4()) is not None: + results.append(val) + print(results) + + print("\nAll counters are independent - they never interfere with each other!") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-15/lesson.md b/src/content/day-15/lesson.md new file mode 100644 index 0000000..e46dd40 --- /dev/null +++ b/src/content/day-15/lesson.md @@ -0,0 +1,191 @@ +# Day 15 🛡️ - Error Handling + +--- + +## Overview + +Write programs that fail gracefully. Learn to catch exceptions, raise your own, and ensure cleanup code always runs - the hallmark of professional Python. + +**What you will learn today:** + +- `try`, `except`, `else`, `finally` blocks +- Common built-in exceptions +- Catching specific vs all exceptions +- `raise` to throw exceptions intentionally +- Creating custom exception classes +- Best practices: what to catch, what to let propagate + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `try/except` | Wrap risky code in `try`. If an exception occurs, the matching `except` block runs instead of crashing. | +| `else` | The `else` block after `try/except` runs only if NO exception was raised. Good for code that should run on success. | +| `finally` | Always runs, whether or not an exception occurred. Use for cleanup (closing files, releasing resources). | +| `raise` | Intentionally raise an exception to signal that something went wrong: `raise ValueError('message')`. | +| `Custom exceptions` | Subclass `Exception` to create domain-specific exceptions that carry meaning in your codebase. | + +--- + +## Code Examples + +### try / except / else / finally + +```python +def read_number(prompt: str) -> float: + """Read a float from input, retrying on invalid input.""" + while True: + try: + value = float(input(prompt)) + except ValueError: + print("Please enter a valid number.") + else: + # Runs only if no exception occurred + return value + finally: + # Always runs (even after return!) + print("(input attempt complete)") + +# Multiple except clauses +try: + result = 10 / 0 +except ZeroDivisionError: + print("Cannot divide by zero.") +except ValueError as e: + print(f"Value error: {e}") +except (TypeError, AttributeError) as e: + print(f"Type/attribute error: {e}") +``` + +### Custom exceptions + +```python +class InsufficientFundsError(Exception): + """Raised when a bank account has insufficient funds.""" + + def __init__(self, amount: float, balance: float) -> None: + self.amount = amount + self.balance = balance + super().__init__( + f"Cannot withdraw ${amount:.2f}: balance is ${balance:.2f}" + ) + +class BankAccount: + def __init__(self, balance: float = 0.0) -> None: + self.balance = balance + + def withdraw(self, amount: float) -> None: + if amount > self.balance: + raise InsufficientFundsError(amount, self.balance) + self.balance -= amount + +account = BankAccount(100.0) +try: + account.withdraw(150.0) +except InsufficientFundsError as e: + print(e) # Cannot withdraw $150.00: balance is $100.00 +``` + +--- + +## Today's Project: Safe Calculator + +> Build a robust four-function calculator that catches every possible error gracefully and never crashes unexpectedly. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 15 Project: Safe Calculator +================================= +A crash-proof calculator with full error handling. +""" + +class CalculatorError(Exception): + """Base exception for calculator errors.""" + +class DivisionByZeroError(CalculatorError): + """Raised when attempting to divide by zero.""" + +class InvalidOperatorError(CalculatorError): + """Raised for unsupported operators.""" + +OPERATORS = { + "+": lambda a, b: a + b, + "-": lambda a, b: a - b, + "*": lambda a, b: a * b, + "/": lambda a, b: a / b, + "//": lambda a, b: a // b, + "**": lambda a, b: a ** b, + "%": lambda a, b: a % b, +} + +def calculate(a: float, op: str, b: float) -> float: + """Perform a calculation safely. + + Raises: + InvalidOperatorError: If the operator is not supported. + DivisionByZeroError: If dividing by zero. + """ + if op not in OPERATORS: + raise InvalidOperatorError(f"Unsupported operator: {op!r}") + if op in ("/", "//", "%") and b == 0: + raise DivisionByZeroError("Cannot divide by zero.") + return OPERATORS[op](a, b) + +def parse_number(text: str, label: str) -> float: + """Parse a string to float, raising ValueError with a clear message.""" + try: + return float(text.strip()) + except ValueError: + raise ValueError(f"Invalid {label}: {text!r} is not a number.") + +def main() -> None: + print("=" * 45) + print(" SAFE CALCULATOR") + print(f" Operators: {', '.join(OPERATORS)}") + print("=" * 45) + print("Type 'quit' as the first number to exit.\n") + + history: list[str] = [] + + while True: + try: + raw_a = input("First number : ").strip() + if raw_a.lower() == "quit": + break + + a = parse_number(raw_a, "first number") + op = input("Operator : ").strip() + b = parse_number(input("Second number : "), "second number") + + result = calculate(a, op, b) + line = f" {a} {op} {b} = {result}" + print(line) + history.append(line.strip()) + + except (CalculatorError, ValueError) as e: + print(f" Error: {e}") + except KeyboardInterrupt: + print("\n Cancelled.") + break + + if history: + print("\n--- Calculation History ---") + for entry in history: + print(f" {entry}") + print("\nGoodbye!") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-15/solution.py b/src/content/day-15/solution.py new file mode 100644 index 0000000..a390281 --- /dev/null +++ b/src/content/day-15/solution.py @@ -0,0 +1,91 @@ +""" +Day 15 Project: Safe Calculator +================================= +A crash-proof calculator with full error handling. +""" + + +class CalculatorError(Exception): + """Base exception for calculator errors.""" + + +class DivisionByZeroError(CalculatorError): + """Raised when attempting to divide by zero.""" + + +class InvalidOperatorError(CalculatorError): + """Raised for unsupported operators.""" + + +OPERATORS = { + "+": lambda a, b: a + b, + "-": lambda a, b: a - b, + "*": lambda a, b: a * b, + "/": lambda a, b: a / b, + "//": lambda a, b: a // b, + "**": lambda a, b: a ** b, + "%": lambda a, b: a % b, +} + + +def calculate(a: float, op: str, b: float) -> float: + """Perform a calculation safely. + + Raises: + InvalidOperatorError: If the operator is not supported. + DivisionByZeroError: If dividing by zero. + """ + if op not in OPERATORS: + raise InvalidOperatorError(f"Unsupported operator: {op!r}") + if op in ("/", "//", "%") and b == 0: + raise DivisionByZeroError("Cannot divide by zero.") + return OPERATORS[op](a, b) + + +def parse_number(text: str, label: str) -> float: + """Parse a string to float, raising ValueError with a clear message.""" + try: + return float(text.strip()) + except ValueError: + raise ValueError(f"Invalid {label}: {text!r} is not a number.") + + +def main() -> None: + print("=" * 45) + print(" SAFE CALCULATOR") + print(f" Operators: {', '.join(OPERATORS)}") + print("=" * 45) + print("Type 'quit' as the first number to exit.\n") + + history: list[str] = [] + + while True: + try: + raw_a = input("First number : ").strip() + if raw_a.lower() == "quit": + break + + a = parse_number(raw_a, "first number") + op = input("Operator : ").strip() + b = parse_number(input("Second number : "), "second number") + + result = calculate(a, op, b) + line = f" {a} {op} {b} = {result}" + print(line) + history.append(line.strip()) + + except (CalculatorError, ValueError) as e: + print(f" Error: {e}") + except KeyboardInterrupt: + print("\n Cancelled.") + break + + if history: + print("\n--- Calculation History ---") + for entry in history: + print(f" {entry}") + print("\nGoodbye!") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-16/lesson.md b/src/content/day-16/lesson.md new file mode 100644 index 0000000..e663887 --- /dev/null +++ b/src/content/day-16/lesson.md @@ -0,0 +1,178 @@ +# Day 16 📁 - File I/O + +--- + +## Overview + +Read and write files with Python - a core skill for data processing, logging, configuration, and storage. + +**What you will learn today:** + +- Opening files with `open()` and the `with` statement +- Reading: `read()`, `readline()`, `readlines()` +- Writing: `write()`, `writelines()` +- File modes: `r`, `w`, `a`, `x`, `b` +- The `pathlib.Path` module (modern approach) +- Working with directories + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `with open()` | Context manager that automatically closes the file, even if an exception occurs. Always prefer this over `open()` + `close()`. | +| `pathlib.Path` | The modern, object-oriented way to work with file system paths. Replaces `os.path` for most use cases. | +| `mode 'r'` | Read mode (default). Raises `FileNotFoundError` if the file does not exist. | +| `mode 'w'` | Write mode. Creates the file if it does not exist. OVERWRITES if it does. | +| `mode 'a'` | Append mode. Creates the file if needed. Adds content to the end without overwriting. | + +--- + +## Code Examples + +### Reading files + +```python +from pathlib import Path + +# Write a sample file first +Path("sample.txt").write_text("Line 1\nLine 2\nLine 3\n") + +# Read entire file at once +content = Path("sample.txt").read_text() +print(content) + +# Read line by line (memory-efficient for large files) +with open("sample.txt", "r") as f: + for line in f: + print(line.strip()) # strip() removes the trailing newline + +# Read all lines into a list +with open("sample.txt", "r") as f: + lines = f.readlines() # ['Line 1\n', 'Line 2\n', ...] +``` + +### Writing and pathlib + +```python +from pathlib import Path + +# Write a file (overwrites if exists) +Path("notes.txt").write_text("My notes\n") + +# Append to a file +with open("notes.txt", "a") as f: + f.write("Second line\n") + f.write("Third line\n") + +# pathlib - modern path operations +p = Path("notes.txt") +print(p.exists()) # True +print(p.name) # notes.txt +print(p.suffix) # .txt +print(p.stem) # notes +print(p.stat().st_size) # file size in bytes + +# List files in a directory +for file in Path(".").glob("*.txt"): + print(file.name) +``` + +--- + +## Today's Project: Personal Diary + +> Build a personal diary application that saves dated entries to a file, and lets you read back past entries. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 16 Project: Personal Diary +================================ +A file-based personal diary with dated entries. +""" +from datetime import datetime +from pathlib import Path + +DIARY_FILE = Path("my_diary.txt") + +def write_entry(text: str) -> None: + """Append a timestamped entry to the diary file.""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") + separator = "-" * 40 + entry = f"\n{separator}\n[{timestamp}]\n{separator}\n{text.strip()}\n" + with open(DIARY_FILE, "a", encoding="utf-8") as f: + f.write(entry) + print(f" Entry saved to {DIARY_FILE}") + +def read_entries(last_n: int | None = None) -> None: + """Read and display diary entries.""" + if not DIARY_FILE.exists(): + print(" No diary entries yet. Start writing!") + return + + content = DIARY_FILE.read_text(encoding="utf-8") + entries = [e.strip() for e in content.split("-" * 40) if e.strip()] + + if last_n: + entries = entries[-last_n * 2:] + + if not entries: + print(" Diary is empty.") + return + + print(f"\n--- Diary ({len(entries) // 2} entry/entries) ---") + print(content if not last_n else "\n" + ("-" * 40 + "\n").join(entries)) + +def main() -> None: + print("=" * 45) + print(" PERSONAL DIARY") + print("=" * 45) + print("Commands: write | read | recent | quit\n") + + while True: + command = input("> ").strip().lower() + + if command == "write": + print(" Write your entry (press Enter twice when done):") + lines = [] + while True: + line = input(" ") + if line == "": + break + lines.append(line) + if lines: + write_entry("\n".join(lines)) + else: + print(" Empty entry not saved.") + + elif command == "read": + read_entries() + + elif command == "recent": + try: + n = int(input(" Show last N entries: ")) + read_entries(last_n=n) + except ValueError: + print(" Please enter a number.") + + elif command == "quit": + print("\nUntil next time!") + break + + else: + print(" Unknown command.") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-16/solution.py b/src/content/day-16/solution.py new file mode 100644 index 0000000..c0411db --- /dev/null +++ b/src/content/day-16/solution.py @@ -0,0 +1,83 @@ +""" +Day 16 Project: Personal Diary +================================ +A file-based personal diary with dated entries. +""" +from datetime import datetime +from pathlib import Path + +DIARY_FILE = Path("my_diary.txt") + + +def write_entry(text: str) -> None: + """Append a timestamped entry to the diary file.""" + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M") + separator = "-" * 40 + entry = f"\n{separator}\n[{timestamp}]\n{separator}\n{text.strip()}\n" + with open(DIARY_FILE, "a", encoding="utf-8") as f: + f.write(entry) + print(f" Entry saved to {DIARY_FILE}") + + +def read_entries(last_n: int | None = None) -> None: + """Read and display diary entries.""" + if not DIARY_FILE.exists(): + print(" No diary entries yet. Start writing!") + return + + content = DIARY_FILE.read_text(encoding="utf-8") + entries = [e.strip() for e in content.split("-" * 40) if e.strip()] + + if last_n: + entries = entries[-last_n * 2:] + + if not entries: + print(" Diary is empty.") + return + + print(f"\n--- Diary ({len(entries) // 2} entry/entries) ---") + print(content if not last_n else "\n" + ("-" * 40 + "\n").join(entries)) + + +def main() -> None: + print("=" * 45) + print(" PERSONAL DIARY") + print("=" * 45) + print("Commands: write | read | recent | quit\n") + + while True: + command = input("> ").strip().lower() + + if command == "write": + print(" Write your entry (press Enter twice when done):") + lines = [] + while True: + line = input(" ") + if line == "": + break + lines.append(line) + if lines: + write_entry("\n".join(lines)) + else: + print(" Empty entry not saved.") + + elif command == "read": + read_entries() + + elif command == "recent": + try: + n = int(input(" Show last N entries: ")) + read_entries(last_n=n) + except ValueError: + print(" Please enter a number.") + + elif command == "quit": + print("\nUntil next time!") + break + + else: + print(" Unknown command.") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-17/lesson.md b/src/content/day-17/lesson.md new file mode 100644 index 0000000..c4d2941 --- /dev/null +++ b/src/content/day-17/lesson.md @@ -0,0 +1,165 @@ +# Day 17 ⚡ - List Comprehensions + +--- + +## Overview + +List comprehensions are one of Python's most beloved features - write concise, readable transformations in a single line. + +**What you will learn today:** + +- List comprehension syntax `[expr for item in iterable]` +- Filtering with conditions: `[expr for item in iterable if cond]` +- Nested comprehensions +- Dictionary comprehensions `{k: v for ...}` +- Set comprehensions `{expr for ...}` +- When to use comprehensions vs regular loops + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `List comprehension` | A compact way to build a list: `[expression for item in iterable if condition]`. More readable than a loop + `append()`. | +| `Dict comprehension` | Build a dictionary: `{key_expr: val_expr for item in iterable}`. Perfect for transforming or filtering dicts. | +| `Set comprehension` | Build a set: `{expr for item in iterable}`. Automatically removes duplicates. | +| `Readability rule` | If a comprehension needs more than one `if` or produces complex logic, use a regular `for` loop instead. | + +--- + +## Code Examples + +### List comprehensions + +```python +# Loop version +squares = [] +for n in range(1, 6): + squares.append(n ** 2) + +# Comprehension version (equivalent, more Pythonic) +squares = [n ** 2 for n in range(1, 6)] +print(squares) # [1, 4, 9, 16, 25] + +# With a condition (filter) +evens = [n for n in range(20) if n % 2 == 0] +print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] + +# Transformation + filter combined +names = ["alice", "bob", "charlie", "dave", "eve"] +long_names = [name.title() for name in names if len(name) > 3] +print(long_names) # ['Alice', 'Charlie', 'Dave'] +``` + +### Dict and set comprehensions + +```python +# Dict comprehension +words = ["hello", "world", "python"] +word_lengths = {word: len(word) for word in words} +print(word_lengths) # {'hello': 5, 'world': 5, 'python': 6} + +# Invert a dictionary +original = {"a": 1, "b": 2, "c": 3} +inverted = {v: k for k, v in original.items()} +print(inverted) # {1: 'a', 2: 'b', 3: 'c'} + +# Set comprehension (unique values) +data = [1, 2, 2, 3, 3, 3, 4] +unique_squares = {n ** 2 for n in data} +print(unique_squares) # {1, 4, 9, 16} +``` + +--- + +## Today's Project: Data Filter Tool + +> Process a list of student records using comprehensions: filter by grade, compute statistics, and transform the data. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 17 Project: Data Filter Tool +================================== +Filter and transform student data using comprehensions. +""" + +STUDENTS = [ + {"name": "Alice", "grade": 92, "subject": "Math"}, + {"name": "Bob", "grade": 71, "subject": "Science"}, + {"name": "Charlie", "grade": 88, "subject": "Math"}, + {"name": "Diana", "grade": 55, "subject": "History"}, + {"name": "Eve", "grade": 95, "subject": "Science"}, + {"name": "Frank", "grade": 63, "subject": "Math"}, + {"name": "Grace", "grade": 79, "subject": "History"}, + {"name": "Henry", "grade": 84, "subject": "Science"}, +] + +def letter_grade(score: int) -> str: + """Convert a numeric score to a letter grade.""" + if score >= 90: + return "A" + elif score >= 80: + return "B" + elif score >= 70: + return "C" + elif score >= 60: + return "D" + return "F" + +def main() -> None: + print("=" * 55) + print(" DATA FILTER TOOL") + print("=" * 55) + + # Add letter grades using a comprehension + enriched = [ + {**s, "letter": letter_grade(s["grade"])} + for s in STUDENTS + ] + + # Students who passed (grade >= 60) + passed = [s["name"] for s in enriched if s["grade"] >= 60] + failed = [s["name"] for s in enriched if s["grade"] < 60] + + # Group by subject + subjects = {s["subject"] for s in STUDENTS} + by_subject = { + subj: [s for s in enriched if s["subject"] == subj] + for subj in sorted(subjects) + } + + # Average per subject + averages = { + subj: sum(s["grade"] for s in students) / len(students) + for subj, students in by_subject.items() + } + + print(f"\nTotal students : {len(STUDENTS)}") + print(f"Passed (>=60) : {len(passed)} - {passed}") + print(f"Failed (<60) : {len(failed)} - {failed}") + + print("\nResults by subject:") + for subj, students in by_subject.items(): + avg = averages[subj] + names = [f"{s['name']} ({s['letter']})" for s in students] + print(f"\n {subj} (avg: {avg:.1f})") + for name in names: + print(f" - {name}") + + top = max(STUDENTS, key=lambda s: s["grade"]) + print(f"\nTop student: {top['name']} with {top['grade']}%") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-17/solution.py b/src/content/day-17/solution.py new file mode 100644 index 0000000..fd28413 --- /dev/null +++ b/src/content/day-17/solution.py @@ -0,0 +1,77 @@ +""" +Day 17 Project: Data Filter Tool +================================== +Filter and transform student data using comprehensions. +""" + +STUDENTS = [ + {"name": "Alice", "grade": 92, "subject": "Math"}, + {"name": "Bob", "grade": 71, "subject": "Science"}, + {"name": "Charlie", "grade": 88, "subject": "Math"}, + {"name": "Diana", "grade": 55, "subject": "History"}, + {"name": "Eve", "grade": 95, "subject": "Science"}, + {"name": "Frank", "grade": 63, "subject": "Math"}, + {"name": "Grace", "grade": 79, "subject": "History"}, + {"name": "Henry", "grade": 84, "subject": "Science"}, +] + + +def letter_grade(score: int) -> str: + """Convert a numeric score to a letter grade.""" + if score >= 90: + return "A" + elif score >= 80: + return "B" + elif score >= 70: + return "C" + elif score >= 60: + return "D" + return "F" + + +def main() -> None: + print("=" * 55) + print(" DATA FILTER TOOL") + print("=" * 55) + + # Add letter grades using a comprehension + enriched = [ + {**s, "letter": letter_grade(s["grade"])} + for s in STUDENTS + ] + + # Students who passed (grade >= 60) + passed = [s["name"] for s in enriched if s["grade"] >= 60] + failed = [s["name"] for s in enriched if s["grade"] < 60] + + # Group by subject + subjects = {s["subject"] for s in STUDENTS} + by_subject = { + subj: [s for s in enriched if s["subject"] == subj] + for subj in sorted(subjects) + } + + # Average per subject + averages = { + subj: sum(s["grade"] for s in students) / len(students) + for subj, students in by_subject.items() + } + + print(f"\nTotal students : {len(STUDENTS)}") + print(f"Passed (>=60) : {len(passed)} - {passed}") + print(f"Failed (<60) : {len(failed)} - {failed}") + + print("\nResults by subject:") + for subj, students in by_subject.items(): + avg = averages[subj] + names = [f"{s['name']} ({s['letter']})" for s in students] + print(f"\n {subj} (avg: {avg:.1f})") + for name in names: + print(f" - {name}") + + top = max(STUDENTS, key=lambda s: s["grade"]) + print(f"\nTop student: {top['name']} with {top['grade']}%") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-18/lesson.md b/src/content/day-18/lesson.md new file mode 100644 index 0000000..174b757 --- /dev/null +++ b/src/content/day-18/lesson.md @@ -0,0 +1,149 @@ +# Day 18 🗃️ - Lambda, Map, Filter & Sorted + +--- + +## Overview + +Functional programming tools in Python: write anonymous functions with `lambda`, transform sequences with `map()` and `filter()`, and sort anything with `sorted()`. + +**What you will learn today:** + +- `lambda` anonymous functions +- `map(func, iterable)` to transform elements +- `filter(func, iterable)` to select elements +- `sorted()` with `key=` argument +- `functools.reduce()` for aggregation +- When to prefer comprehensions over functional tools + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `lambda` | An anonymous single-expression function. `lambda x: x * 2` is equivalent to `def f(x): return x * 2`. Use for short, throwaway functions. | +| `map()` | Applies a function to every item in an iterable. Returns a `map` object (lazy); wrap in `list()` to see results. | +| `filter()` | Keeps items where the function returns `True`. Returns a `filter` object (lazy). | +| `sorted() key=` | The `key` argument accepts a function applied to each element before comparing. Very powerful for custom sorts. | + +--- + +## Code Examples + +### lambda and map + +```python +# lambda: anonymous functions +double = lambda x: x * 2 +print(double(5)) # 10 + +# More natural as an argument to another function +numbers = [1, 2, 3, 4, 5] +doubled = list(map(lambda x: x * 2, numbers)) +print(doubled) # [2, 4, 6, 8, 10] + +# map with a named function +def celsius_to_fahrenheit(c: float) -> float: + return c * 9/5 + 32 + +temps_c = [0, 20, 37, 100] +temps_f = list(map(celsius_to_fahrenheit, temps_c)) +print(temps_f) # [32.0, 68.0, 98.6, 212.0] +``` + +### filter and sorted + +```python +numbers = [1, -2, 3, -4, 5, -6, 7] + +# filter keeps items where function returns True +positives = list(filter(lambda x: x > 0, numbers)) +print(positives) # [1, 3, 5, 7] + +# sorted() with key function +words = ["banana", "apple", "cherry", "fig", "elderberry"] +by_length = sorted(words, key=len) +print(by_length) # ['fig', 'apple', 'banana', 'cherry', 'elderberry'] + +# Sort complex objects +people = [ + {"name": "Bob", "age": 30}, + {"name": "Alice", "age": 25}, + {"name": "Charlie", "age": 35}, +] +by_age = sorted(people, key=lambda p: p["age"]) +print([p["name"] for p in by_age]) # ['Alice', 'Bob', 'Charlie'] +``` + +--- + +## Today's Project: Functional Data Pipeline + +> Process a sales dataset using `map()`, `filter()`, `sorted()`, and `reduce()` to demonstrate functional data transformation. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 18 Project: Functional Pipeline +===================================== +Process sales data using functional tools. +""" +from functools import reduce + +SALES = [ + {"product": "Laptop", "price": 999.99, "qty": 3, "category": "Electronics"}, + {"product": "Phone", "price": 699.99, "qty": 7, "category": "Electronics"}, + {"product": "Desk", "price": 249.99, "qty": 2, "category": "Furniture"}, + {"product": "Chair", "price": 189.99, "qty": 5, "category": "Furniture"}, + {"product": "Headphones", "price": 149.99, "qty": 12, "category": "Electronics"}, + {"product": "Lamp", "price": 49.99, "qty": 8, "category": "Furniture"}, + {"product": "Keyboard", "price": 79.99, "qty": 15, "category": "Electronics"}, +] + +def add_revenue(sale: dict) -> dict: + """Return sale record with revenue field added.""" + return {**sale, "revenue": sale["price"] * sale["qty"]} + +def main() -> None: + print("=" * 55) + print(" SALES DATA PIPELINE") + print("=" * 55) + + # Step 1: Calculate revenue for each item (map) + with_revenue = list(map(add_revenue, SALES)) + + # Step 2: Filter high-value items only (filter) + high_value = list(filter(lambda s: s["revenue"] > 500, with_revenue)) + + # Step 3: Sort by revenue descending (sorted) + ranked = sorted(high_value, key=lambda s: s["revenue"], reverse=True) + + # Step 4: Total revenue of all items (reduce) + total = reduce(lambda acc, s: acc + s["revenue"], with_revenue, 0.0) + + print("\nTop Revenue Items:") + print(f" {'Product':<15} {'Price':>8} {'Qty':>5} {'Revenue':>10}") + print(" " + "-" * 42) + for sale in ranked: + print(f" {sale['product']:<15} ${sale['price']:>7.2f} {sale['qty']:>5} ${sale['revenue']:>9.2f}") + + print(f"\n Total Revenue (all items): ${total:,.2f}") + + # Bonus: Electronics vs Furniture totals + categories = {s["category"] for s in SALES} + for cat in sorted(categories): + cat_total = sum(s["revenue"] for s in with_revenue if s["category"] == cat) + print(f" {cat} Revenue: ${cat_total:,.2f}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-18/solution.py b/src/content/day-18/solution.py new file mode 100644 index 0000000..6ca4669 --- /dev/null +++ b/src/content/day-18/solution.py @@ -0,0 +1,57 @@ +""" +Day 18 Project: Functional Pipeline +===================================== +Process sales data using functional tools. +""" +from functools import reduce + +SALES = [ + {"product": "Laptop", "price": 999.99, "qty": 3, "category": "Electronics"}, + {"product": "Phone", "price": 699.99, "qty": 7, "category": "Electronics"}, + {"product": "Desk", "price": 249.99, "qty": 2, "category": "Furniture"}, + {"product": "Chair", "price": 189.99, "qty": 5, "category": "Furniture"}, + {"product": "Headphones", "price": 149.99, "qty": 12, "category": "Electronics"}, + {"product": "Lamp", "price": 49.99, "qty": 8, "category": "Furniture"}, + {"product": "Keyboard", "price": 79.99, "qty": 15, "category": "Electronics"}, +] + + +def add_revenue(sale: dict) -> dict: + """Return sale record with revenue field added.""" + return {**sale, "revenue": sale["price"] * sale["qty"]} + + +def main() -> None: + print("=" * 55) + print(" SALES DATA PIPELINE") + print("=" * 55) + + # Step 1: Calculate revenue for each item (map) + with_revenue = list(map(add_revenue, SALES)) + + # Step 2: Filter high-value items only (filter) + high_value = list(filter(lambda s: s["revenue"] > 500, with_revenue)) + + # Step 3: Sort by revenue descending (sorted) + ranked = sorted(high_value, key=lambda s: s["revenue"], reverse=True) + + # Step 4: Total revenue of all items (reduce) + total = reduce(lambda acc, s: acc + s["revenue"], with_revenue, 0.0) + + print("\nTop Revenue Items:") + print(f" {'Product':<15} {'Price':>8} {'Qty':>5} {'Revenue':>10}") + print(" " + "-" * 42) + for sale in ranked: + print(f" {sale['product']:<15} ${sale['price']:>7.2f} {sale['qty']:>5} ${sale['revenue']:>9.2f}") + + print(f"\n Total Revenue (all items): ${total:,.2f}") + + # Bonus: Electronics vs Furniture totals + categories = {s["category"] for s in SALES} + for cat in sorted(categories): + cat_total = sum(s["revenue"] for s in with_revenue if s["category"] == cat) + print(f" {cat} Revenue: ${cat_total:,.2f}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-19/lesson.md b/src/content/day-19/lesson.md new file mode 100644 index 0000000..79e8283 --- /dev/null +++ b/src/content/day-19/lesson.md @@ -0,0 +1,191 @@ +# Day 19 📦 - Modules & Packages + +--- + +## Overview + +Learn to organize your code into reusable modules, use the Python standard library, and manage third-party packages with `pip`. + +**What you will learn today:** + +- Creating and importing your own modules +- `from module import name` vs `import module` +- The `__name__ == '__main__'` guard +- Exploring the Python standard library +- Installing packages with `pip` +- Virtual environments with `venv` + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `module` | Any `.py` file is a module. Import it with `import filename` (without the `.py`). | +| `package` | A directory containing an `__init__.py` file, grouping related modules together. | +| `__name__` | Set to `'__main__'` when a file is run directly, or to the module name when imported. The guard `if __name__ == '__main__':` prevents code from running on import. | +| `venv` | A virtual environment is an isolated Python installation for a project. Best practice for every project. | + +--- + +## Code Examples + +### Importing modules + +```python +# Import the whole module (access via module.function) +import math +print(math.sqrt(16)) # 4.0 +print(math.pi) # 3.14159... + +# Import specific names +from math import sqrt, pi +print(sqrt(16)) # 4.0 (no prefix needed) + +# Import with an alias +import datetime as dt +today = dt.date.today() +print(today) + +# Import all names (avoid in production - pollutes namespace) +from os.path import * + +# Standard library highlights +import random +import json +import os +import sys +import re +import collections +import itertools +``` + +### Virtual environments and pip + +```python +# Create a virtual environment +# python3 -m venv venv + +# Activate it: +# macOS/Linux: source venv/bin/activate +# Windows: venv\Scripts\activate + +# Install packages +# pip install requests +# pip install "requests==2.31.0" # pin a version + +# Save dependencies +# pip freeze > requirements.txt + +# Install from requirements +# pip install -r requirements.txt + +# Deactivate +# deactivate + +# Check installed packages +# pip list +# pip show requests +``` + +--- + +## Today's Project: Random Quote CLI + +> Build a modular quote application with a separate quotes module, display module, and main runner. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 19 Project: Random Quote CLI +================================== +A modular quote application demonstrating imports. + +Structure: + solution.py (main runner - this file) +""" +import random +from datetime import date + +# Inline module simulation (in a real project, these would be separate files) + +QUOTES = [ + {"text": "The only way to do great work is to love what you do.", "author": "Steve Jobs"}, + {"text": "In the beginning was the Word, and the Word was Python.", "author": "Unknown Pythonista"}, + {"text": "First, solve the problem. Then, write the code.", "author": "John Johnson"}, + {"text": "Experience is the name everyone gives to their mistakes.", "author": "Oscar Wilde"}, + {"text": "Code is like humor. When you have to explain it, it is bad.", "author": "Cory House"}, + {"text": "Programs must be written for people to read, and only incidentally for machines.", "author": "Abelson & Sussman"}, + {"text": "The best error message is the one that never shows up.", "author": "Thomas Fuchs"}, + {"text": "Make it work, make it right, make it fast.", "author": "Kent Beck"}, +] + +def get_random_quote(quotes: list[dict]) -> dict: + """Return a random quote from the collection.""" + return random.choice(quotes) + +def get_quote_of_the_day(quotes: list[dict]) -> dict: + """Return a deterministic quote based on today's date.""" + index = date.today().toordinal() % len(quotes) + return quotes[index] + +def format_quote(quote: dict, width: int = 60) -> str: + """Format a quote for display.""" + text = quote["text"] + author = quote["author"] + border = "=" * width + return f"\n{border}\n\n {text}\n\n - {author}\n\n{border}" + +def search_quotes(query: str, quotes: list[dict]) -> list[dict]: + """Search quotes by text or author (case-insensitive).""" + q = query.lower() + return [quote for quote in quotes if q in quote["text"].lower() or q in quote["author"].lower()] + +def main() -> None: + print("=" * 60) + print(" RANDOM QUOTE CLI") + print("=" * 60) + print("Commands: random | today | search | list | quit\n") + + while True: + cmd = input("> ").strip().lower() + + if cmd == "random": + print(format_quote(get_random_quote(QUOTES))) + + elif cmd == "today": + print("Quote of the day:") + print(format_quote(get_quote_of_the_day(QUOTES))) + + elif cmd == "search": + query = input(" Search term: ").strip() + results = search_quotes(query, QUOTES) + if results: + for q in results: + print(format_quote(q)) + else: + print(f" No quotes matching '{query}'.") + + elif cmd == "list": + for i, q in enumerate(QUOTES, 1): + print(f" {i}. {q['author']}: {q['text'][:50]}...") + + elif cmd == "quit": + print("\nStay inspired!") + break + + else: + print(" Unknown command.") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-19/solution.py b/src/content/day-19/solution.py new file mode 100644 index 0000000..5787155 --- /dev/null +++ b/src/content/day-19/solution.py @@ -0,0 +1,89 @@ +""" +Day 19 Project: Random Quote CLI +================================== +A modular quote application demonstrating imports. + +Structure: + solution.py (main runner - this file) +""" +import random +from datetime import date + +# Inline module simulation (in a real project, these would be separate files) + +QUOTES = [ + {"text": "The only way to do great work is to love what you do.", "author": "Steve Jobs"}, + {"text": "In the beginning was the Word, and the Word was Python.", "author": "Unknown Pythonista"}, + {"text": "First, solve the problem. Then, write the code.", "author": "John Johnson"}, + {"text": "Experience is the name everyone gives to their mistakes.", "author": "Oscar Wilde"}, + {"text": "Code is like humor. When you have to explain it, it is bad.", "author": "Cory House"}, + {"text": "Programs must be written for people to read, and only incidentally for machines.", "author": "Abelson & Sussman"}, + {"text": "The best error message is the one that never shows up.", "author": "Thomas Fuchs"}, + {"text": "Make it work, make it right, make it fast.", "author": "Kent Beck"}, +] + + +def get_random_quote(quotes: list[dict]) -> dict: + """Return a random quote from the collection.""" + return random.choice(quotes) + + +def get_quote_of_the_day(quotes: list[dict]) -> dict: + """Return a deterministic quote based on today's date.""" + index = date.today().toordinal() % len(quotes) + return quotes[index] + + +def format_quote(quote: dict, width: int = 60) -> str: + """Format a quote for display.""" + text = quote["text"] + author = quote["author"] + border = "=" * width + return f"\n{border}\n\n {text}\n\n - {author}\n\n{border}" + + +def search_quotes(query: str, quotes: list[dict]) -> list[dict]: + """Search quotes by text or author (case-insensitive).""" + q = query.lower() + return [quote for quote in quotes if q in quote["text"].lower() or q in quote["author"].lower()] + + +def main() -> None: + print("=" * 60) + print(" RANDOM QUOTE CLI") + print("=" * 60) + print("Commands: random | today | search | list | quit\n") + + while True: + cmd = input("> ").strip().lower() + + if cmd == "random": + print(format_quote(get_random_quote(QUOTES))) + + elif cmd == "today": + print("Quote of the day:") + print(format_quote(get_quote_of_the_day(QUOTES))) + + elif cmd == "search": + query = input(" Search term: ").strip() + results = search_quotes(query, QUOTES) + if results: + for q in results: + print(format_quote(q)) + else: + print(f" No quotes matching '{query}'.") + + elif cmd == "list": + for i, q in enumerate(QUOTES, 1): + print(f" {i}. {q['author']}: {q['text'][:50]}...") + + elif cmd == "quit": + print("\nStay inspired!") + break + + else: + print(" Unknown command.") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-20/lesson.md b/src/content/day-20/lesson.md new file mode 100644 index 0000000..a5bec64 --- /dev/null +++ b/src/content/day-20/lesson.md @@ -0,0 +1,192 @@ +# Day 20 🏗️ - OOP: Classes & Objects + +--- + +## Overview + +Object-Oriented Programming lets you model real-world entities as objects that bundle data and behavior together. This is how most large Python programs are structured. + +**What you will learn today:** + +- Defining classes with `class` +- The `__init__` constructor method +- Instance attributes and methods +- Class attributes (shared by all instances) +- `self` explained +- Creating and using objects + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `class` | A blueprint for creating objects. Defines the data (attributes) and behaviors (methods) that all instances share. | +| `__init__` | The constructor. Called automatically when you create a new instance: `MyClass(args)`. | +| `self` | A reference to the current instance. Always the first parameter of instance methods. Python passes it automatically. | +| `instance attribute` | Data belonging to a specific object, set with `self.name = value` inside methods. | +| `class attribute` | Data shared by ALL instances of the class, defined at the class body level. | + +--- + +## Code Examples + +### Defining a class + +```python +class Dog: + """Represents a dog.""" + + species = "Canis lupus familiaris" # class attribute (shared) + + def __init__(self, name: str, age: int, breed: str) -> None: + """Initialize a Dog instance.""" + self.name = name # instance attribute + self.age = age + self.breed = breed + + def bark(self) -> str: + """Return the dog's bark.""" + return f"{self.name} says: Woof!" + + def birthday(self) -> None: + """Increment the dog's age by 1.""" + self.age += 1 + print(f"Happy birthday, {self.name}! Now {self.age} years old.") + + def __repr__(self) -> str: + return f"Dog(name={self.name!r}, age={self.age}, breed={self.breed!r})" + +# Create instances +rex = Dog("Rex", 3, "German Shepherd") +buddy = Dog("Buddy", 5, "Golden Retriever") + +print(rex.bark()) # Rex says: Woof! +print(buddy.name) # Buddy +print(Dog.species) # Canis lupus familiaris (class attribute) +rex.birthday() # Happy birthday, Rex! Now 4 years old. +print(repr(rex)) +``` + +--- + +## Today's Project: Bank Account System + +> Build a complete bank account class with deposit, withdrawal, transfer, and transaction history. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 20 Project: Bank Account System +===================================== +OOP bank account with transaction history. +""" +from datetime import datetime + +class InsufficientFundsError(Exception): + """Raised when account has insufficient funds.""" + +class BankAccount: + """A simple bank account with transaction history. + + Attributes: + owner: Full name of the account holder. + account_number: Unique account identifier. + balance: Current balance (read-only via property). + """ + + _next_account_number = 1000 + + def __init__(self, owner: str, initial_deposit: float = 0.0) -> None: + self.owner = owner + self.account_number = BankAccount._next_account_number + BankAccount._next_account_number += 1 + self._balance = 0.0 + self._transactions: list[dict] = [] + if initial_deposit > 0: + self.deposit(initial_deposit) + + @property + def balance(self) -> float: + """Current account balance (read-only).""" + return self._balance + + def _record(self, kind: str, amount: float, note: str = "") -> None: + self._transactions.append({ + "type": kind, + "amount": amount, + "balance": self._balance, + "time": datetime.now().strftime("%Y-%m-%d %H:%M"), + "note": note, + }) + + def deposit(self, amount: float) -> None: + """Deposit a positive amount into the account.""" + if amount <= 0: + raise ValueError("Deposit amount must be positive.") + self._balance += amount + self._record("DEPOSIT", amount) + + def withdraw(self, amount: float) -> None: + """Withdraw from the account if funds are sufficient.""" + if amount <= 0: + raise ValueError("Withdrawal amount must be positive.") + if amount > self._balance: + raise InsufficientFundsError( + f"Tried to withdraw ${amount:.2f}, but balance is ${self._balance:.2f}" + ) + self._balance -= amount + self._record("WITHDRAW", amount) + + def transfer(self, amount: float, target: "BankAccount") -> None: + """Transfer funds to another account.""" + self.withdraw(amount) + target.deposit(amount) + self._transactions[-1]["note"] = f"to #{target.account_number}" + target._transactions[-1]["note"] = f"from #{self.account_number}" + + def statement(self) -> str: + """Return a formatted account statement.""" + lines = [ + f"\n{'=' * 50}", + f" Account: #{self.account_number} | Owner: {self.owner}", + f" Current Balance: ${self._balance:,.2f}", + f"{'=' * 50}", + f" {'Type':<10} {'Amount':>10} {'Balance':>10} Time", + f" {'-' * 46}", + ] + for t in self._transactions: + sign = "+" if t["type"] == "DEPOSIT" else "-" + note = f" ({t['note']})" if t["note"] else "" + lines.append( + f" {t['type']:<10} {sign}${t['amount']:>8.2f} ${t['balance']:>9.2f} {t['time']}{note}" + ) + lines.append("=" * 50) + return "\n".join(lines) + + def __repr__(self) -> str: + return f"BankAccount(owner={self.owner!r}, balance=${self._balance:.2f})" + +def main() -> None: + alice = BankAccount("Alice Johnson", initial_deposit=1000.0) + bob = BankAccount("Bob Smith", initial_deposit=500.0) + + alice.deposit(250.0) + alice.withdraw(100.0) + alice.transfer(200.0, bob) + + print(alice.statement()) + print(bob.statement()) + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-20/solution.py b/src/content/day-20/solution.py new file mode 100644 index 0000000..e676a53 --- /dev/null +++ b/src/content/day-20/solution.py @@ -0,0 +1,108 @@ +""" +Day 20 Project: Bank Account System +===================================== +OOP bank account with transaction history. +""" +from datetime import datetime + + +class InsufficientFundsError(Exception): + """Raised when account has insufficient funds.""" + + +class BankAccount: + """A simple bank account with transaction history. + + Attributes: + owner: Full name of the account holder. + account_number: Unique account identifier. + balance: Current balance (read-only via property). + """ + + _next_account_number = 1000 + + def __init__(self, owner: str, initial_deposit: float = 0.0) -> None: + self.owner = owner + self.account_number = BankAccount._next_account_number + BankAccount._next_account_number += 1 + self._balance = 0.0 + self._transactions: list[dict] = [] + if initial_deposit > 0: + self.deposit(initial_deposit) + + @property + def balance(self) -> float: + """Current account balance (read-only).""" + return self._balance + + def _record(self, kind: str, amount: float, note: str = "") -> None: + self._transactions.append({ + "type": kind, + "amount": amount, + "balance": self._balance, + "time": datetime.now().strftime("%Y-%m-%d %H:%M"), + "note": note, + }) + + def deposit(self, amount: float) -> None: + """Deposit a positive amount into the account.""" + if amount <= 0: + raise ValueError("Deposit amount must be positive.") + self._balance += amount + self._record("DEPOSIT", amount) + + def withdraw(self, amount: float) -> None: + """Withdraw from the account if funds are sufficient.""" + if amount <= 0: + raise ValueError("Withdrawal amount must be positive.") + if amount > self._balance: + raise InsufficientFundsError( + f"Tried to withdraw ${amount:.2f}, but balance is ${self._balance:.2f}" + ) + self._balance -= amount + self._record("WITHDRAW", amount) + + def transfer(self, amount: float, target: "BankAccount") -> None: + """Transfer funds to another account.""" + self.withdraw(amount) + target.deposit(amount) + self._transactions[-1]["note"] = f"to #{target.account_number}" + target._transactions[-1]["note"] = f"from #{self.account_number}" + + def statement(self) -> str: + """Return a formatted account statement.""" + lines = [ + f"\n{'=' * 50}", + f" Account: #{self.account_number} | Owner: {self.owner}", + f" Current Balance: ${self._balance:,.2f}", + f"{'=' * 50}", + f" {'Type':<10} {'Amount':>10} {'Balance':>10} Time", + f" {'-' * 46}", + ] + for t in self._transactions: + sign = "+" if t["type"] == "DEPOSIT" else "-" + note = f" ({t['note']})" if t["note"] else "" + lines.append( + f" {t['type']:<10} {sign}${t['amount']:>8.2f} ${t['balance']:>9.2f} {t['time']}{note}" + ) + lines.append("=" * 50) + return "\n".join(lines) + + def __repr__(self) -> str: + return f"BankAccount(owner={self.owner!r}, balance=${self._balance:.2f})" + + +def main() -> None: + alice = BankAccount("Alice Johnson", initial_deposit=1000.0) + bob = BankAccount("Bob Smith", initial_deposit=500.0) + + alice.deposit(250.0) + alice.withdraw(100.0) + alice.transfer(200.0, bob) + + print(alice.statement()) + print(bob.statement()) + + +if __name__ == "__main__": + main() diff --git a/src/content/day-21/lesson.md b/src/content/day-21/lesson.md new file mode 100644 index 0000000..7dcca10 --- /dev/null +++ b/src/content/day-21/lesson.md @@ -0,0 +1,228 @@ +# Day 21 🧬 - OOP: Inheritance & Polymorphism + +--- + +## Overview + +Inheritance lets you build new classes on top of existing ones, reusing and extending their behavior. Polymorphism means different classes can share the same interface. + +**What you will learn today:** + +- Inheritance: `class Child(Parent)` +- Calling parent methods with `super()` +- Overriding methods +- Multiple inheritance (and when to avoid it) +- `isinstance()` and `issubclass()` +- Abstract base classes with `abc` + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `inheritance` | A class can inherit all attributes and methods from a parent class. The child can override or extend them. | +| `super()` | Calls a method from the parent class. Essential when overriding `__init__` to properly initialize the parent. | +| `polymorphism` | Different classes can implement the same method name. Code that calls `animal.speak()` works for any animal type. | +| `ABC` | Abstract Base Class from the `abc` module. Forces subclasses to implement specific methods. | + +--- + +## Code Examples + +### Inheritance + +```python +class Animal: + """Base class for all animals.""" + + def __init__(self, name: str, sound: str) -> None: + self.name = name + self.sound = sound + + def speak(self) -> str: + return f"{self.name} says {self.sound}!" + + def __repr__(self) -> str: + return f"{type(self).__name__}({self.name!r})" + +class Dog(Animal): + def __init__(self, name: str) -> None: + super().__init__(name, "Woof") # Call parent __init__ + + def fetch(self, item: str) -> str: + return f"{self.name} fetches the {item}!" + +class Cat(Animal): + def __init__(self, name: str) -> None: + super().__init__(name, "Meow") + + def speak(self) -> str: # Override parent method + return f"{self.name} says {self.sound}... and ignores you." + +# Polymorphism in action +animals: list[Animal] = [Dog("Rex"), Cat("Whiskers"), Dog("Buddy")] +for animal in animals: + print(animal.speak()) # Each calls its own version of speak() +``` + +### Abstract Base Classes + +```python +from abc import ABC, abstractmethod + +class Shape(ABC): + """Abstract shape. All subclasses MUST implement area() and perimeter().""" + + @abstractmethod + def area(self) -> float: + """Return the area of the shape.""" + ... + + @abstractmethod + def perimeter(self) -> float: + """Return the perimeter of the shape.""" + ... + + def describe(self) -> str: + return f"{type(self).__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}" + +class Circle(Shape): + def __init__(self, radius: float) -> None: + self.radius = radius + + def area(self) -> float: + import math + return math.pi * self.radius ** 2 + + def perimeter(self) -> float: + import math + return 2 * math.pi * self.radius +``` + +--- + +## Today's Project: Animal Kingdom + +> Build an animal class hierarchy with a base `Animal` class and multiple subclasses, each with unique behaviors. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 21 Project: Animal Kingdom +================================ +Polymorphic animal class hierarchy. +""" +from abc import ABC, abstractmethod +import math + +class Animal(ABC): + """Abstract base class for all animals.""" + + def __init__(self, name: str, age: int) -> None: + self.name = name + self.age = age + self._hunger = 5 # 0 (full) to 10 (starving) + + @property + def hunger(self) -> int: + return self._hunger + + @abstractmethod + def speak(self) -> str: + """Return the animal's vocalization.""" + ... + + @abstractmethod + def move(self) -> str: + """Return a description of how the animal moves.""" + ... + + def eat(self, food: str) -> str: + self._hunger = max(0, self._hunger - 3) + return f"{self.name} eats {food}. Hunger: {self._hunger}/10" + + def __str__(self) -> str: + return f"{type(self).__name__}(name={self.name!r}, age={self.age})" + +class Dog(Animal): + def __init__(self, name: str, age: int, breed: str) -> None: + super().__init__(name, age) + self.breed = breed + + def speak(self) -> str: + return f"{self.name} barks: WOOF WOOF!" + + def move(self) -> str: + return f"{self.name} runs happily toward you." + + def fetch(self, item: str) -> str: + return f"{self.name} ({self.breed}) fetches the {item}!" + +class Cat(Animal): + def __init__(self, name: str, age: int, indoor: bool = True) -> None: + super().__init__(name, age) + self.indoor = indoor + + def speak(self) -> str: + return f"{self.name} meows softly... then walks away." + + def move(self) -> str: + return f"{self.name} slinks silently across the room." + + def purr(self) -> str: + return f"{self.name} purrs contentedly." + +class Bird(Animal): + def __init__(self, name: str, age: int, can_fly: bool = True) -> None: + super().__init__(name, age) + self.can_fly = can_fly + + def speak(self) -> str: + return f"{self.name} chirps melodically." + + def move(self) -> str: + if self.can_fly: + return f"{self.name} soars gracefully through the air." + return f"{self.name} waddles along the ground." + +def run_zoo(animals: list[Animal]) -> None: + """Demonstrate polymorphism: same interface, different behaviors.""" + print("\n--- Zoo Report ---") + for animal in animals: + print(f"\n{animal}") + print(f" {animal.speak()}") + print(f" {animal.move()}") + print(f" {animal.eat('food')}") + +def main() -> None: + print("=" * 50) + print(" ANIMAL KINGDOM") + print("=" * 50) + + zoo: list[Animal] = [ + Dog("Rex", 3, "German Shepherd"), + Cat("Whiskers", 5), + Bird("Tweety", 2, can_fly=True), + Dog("Buddy", 7, "Labrador"), + Bird("Pingu", 4, can_fly=False), + ] + + run_zoo(zoo) + + print(f"\n--- Type Checking ---") + for animal in zoo: + print(f" {animal.name}: Dog? {isinstance(animal, Dog)}, Animal? {isinstance(animal, Animal)}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-21/solution.py b/src/content/day-21/solution.py new file mode 100644 index 0000000..e603806 --- /dev/null +++ b/src/content/day-21/solution.py @@ -0,0 +1,115 @@ +""" +Day 21 Project: Animal Kingdom +================================ +Polymorphic animal class hierarchy. +""" +from abc import ABC, abstractmethod +import math + + +class Animal(ABC): + """Abstract base class for all animals.""" + + def __init__(self, name: str, age: int) -> None: + self.name = name + self.age = age + self._hunger = 5 # 0 (full) to 10 (starving) + + @property + def hunger(self) -> int: + return self._hunger + + @abstractmethod + def speak(self) -> str: + """Return the animal's vocalization.""" + ... + + @abstractmethod + def move(self) -> str: + """Return a description of how the animal moves.""" + ... + + def eat(self, food: str) -> str: + self._hunger = max(0, self._hunger - 3) + return f"{self.name} eats {food}. Hunger: {self._hunger}/10" + + def __str__(self) -> str: + return f"{type(self).__name__}(name={self.name!r}, age={self.age})" + + +class Dog(Animal): + def __init__(self, name: str, age: int, breed: str) -> None: + super().__init__(name, age) + self.breed = breed + + def speak(self) -> str: + return f"{self.name} barks: WOOF WOOF!" + + def move(self) -> str: + return f"{self.name} runs happily toward you." + + def fetch(self, item: str) -> str: + return f"{self.name} ({self.breed}) fetches the {item}!" + + +class Cat(Animal): + def __init__(self, name: str, age: int, indoor: bool = True) -> None: + super().__init__(name, age) + self.indoor = indoor + + def speak(self) -> str: + return f"{self.name} meows softly... then walks away." + + def move(self) -> str: + return f"{self.name} slinks silently across the room." + + def purr(self) -> str: + return f"{self.name} purrs contentedly." + + +class Bird(Animal): + def __init__(self, name: str, age: int, can_fly: bool = True) -> None: + super().__init__(name, age) + self.can_fly = can_fly + + def speak(self) -> str: + return f"{self.name} chirps melodically." + + def move(self) -> str: + if self.can_fly: + return f"{self.name} soars gracefully through the air." + return f"{self.name} waddles along the ground." + + +def run_zoo(animals: list[Animal]) -> None: + """Demonstrate polymorphism: same interface, different behaviors.""" + print("\n--- Zoo Report ---") + for animal in animals: + print(f"\n{animal}") + print(f" {animal.speak()}") + print(f" {animal.move()}") + print(f" {animal.eat('food')}") + + +def main() -> None: + print("=" * 50) + print(" ANIMAL KINGDOM") + print("=" * 50) + + zoo: list[Animal] = [ + Dog("Rex", 3, "German Shepherd"), + Cat("Whiskers", 5), + Bird("Tweety", 2, can_fly=True), + Dog("Buddy", 7, "Labrador"), + Bird("Pingu", 4, can_fly=False), + ] + + run_zoo(zoo) + + print(f"\n--- Type Checking ---") + for animal in zoo: + print(f" {animal.name}: Dog? {isinstance(animal, Dog)}, Animal? {isinstance(animal, Animal)}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-22/lesson.md b/src/content/day-22/lesson.md new file mode 100644 index 0000000..515a1f6 --- /dev/null +++ b/src/content/day-22/lesson.md @@ -0,0 +1,197 @@ +# Day 22 🧙 - Dunder Methods + +--- + +## Overview + +Dunder methods (double underscore methods) let your classes behave like Python builtins. This is how Python's data model works under the hood. + +**What you will learn today:** + +- What dunder methods are and why they matter +- `__str__` and `__repr__` for string representation +- `__len__`, `__getitem__`, `__contains__` for container behavior +- `__eq__`, `__lt__`, `__add__` for operator overloading +- `__call__` to make objects callable +- The full Python data model + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `__str__` | Called by `str(obj)` and `print(obj)`. Return a human-readable string. | +| `__repr__` | Called by `repr(obj)` and in the REPL. Return an unambiguous representation, ideally one that could recreate the object. | +| `__len__` | Called by `len(obj)`. Return an integer. | +| `__eq__` | Called by `==`. Define what equality means for your objects. | +| `__add__` | Called by `+`. Enables `obj1 + obj2`. | + +--- + +## Code Examples + +### Core dunder methods + +```python +class Vector: + """2D vector with operator support.""" + + def __init__(self, x: float, y: float) -> None: + self.x = x + self.y = y + + def __repr__(self) -> str: + return f"Vector({self.x}, {self.y})" + + def __str__(self) -> str: + return f"({self.x}, {self.y})" + + def __add__(self, other: "Vector") -> "Vector": + return Vector(self.x + other.x, self.y + other.y) + + def __mul__(self, scalar: float) -> "Vector": + return Vector(self.x * scalar, self.y * scalar) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Vector): + return NotImplemented + return self.x == other.x and self.y == other.y + + def __len__(self) -> int: + import math + return int(math.sqrt(self.x**2 + self.y**2)) + +v1 = Vector(1, 2) +v2 = Vector(3, 4) +print(v1 + v2) # (4, 6) +print(v1 * 3) # (3, 6) +print(v1 == Vector(1, 2)) # True +print(len(v2)) # 5 +``` + +--- + +## Today's Project: Custom Stack Data Structure + +> Build a `Stack` class with full dunder method support that behaves naturally with Python built-in operations. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 22 Project: Custom Stack Data Structure +============================================= +A Stack class with complete dunder method support. +""" +from typing import Generic, TypeVar + +T = TypeVar("T") + +class EmptyStackError(Exception): + """Raised when popping from an empty stack.""" + +class Stack(Generic[T]): + """A LIFO stack data structure with full Python data model support. + + Supports: + len(), bool(), in, iteration, indexing, ==, +, repr(), str() + """ + + def __init__(self, *items: T) -> None: + self._data: list[T] = list(items) + + def push(self, item: T) -> None: + """Push an item onto the top of the stack.""" + self._data.append(item) + + def pop(self) -> T: + """Remove and return the top item.""" + if not self._data: + raise EmptyStackError("Cannot pop from an empty stack.") + return self._data.pop() + + def peek(self) -> T: + """Return the top item without removing it.""" + if not self._data: + raise EmptyStackError("Stack is empty.") + return self._data[-1] + + # --- Dunder Methods --- + + def __len__(self) -> int: + return len(self._data) + + def __bool__(self) -> bool: + return bool(self._data) + + def __contains__(self, item: object) -> bool: + return item in self._data + + def __iter__(self): + """Iterate from top to bottom.""" + return reversed(self._data) + + def __getitem__(self, index: int) -> T: + """Access by index from top (index 0 = top).""" + return self._data[-(index + 1)] + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Stack): + return NotImplemented + return self._data == other._data + + def __add__(self, other: "Stack[T]") -> "Stack[T]": + """Combine two stacks (other on top of self).""" + new = Stack(*self._data) + for item in other._data: + new.push(item) + return new + + def __repr__(self) -> str: + return f"Stack({self._data!r})" + + def __str__(self) -> str: + if not self._data: + return "Stack: [empty]" + items = " -> ".join(str(item) for item in reversed(self._data)) + return f"Stack: [{items}] (top ->)" + +def main() -> None: + print("=" * 50) + print(" CUSTOM STACK DEMO") + print("=" * 50) + + s: Stack[int] = Stack(1, 2, 3) + s.push(4) + s.push(5) + + print(f"\nStack: {s}") + print(f"len(s): {len(s)}") + print(f"bool(s): {bool(s)}") + print(f"3 in s: {3 in s}") + print(f"s[0] (top): {s[0]}") + print(f"Peek: {s.peek()}") + + print("\nIterating (top to bottom):") + for item in s: + print(f" {item}") + + s2: Stack[int] = Stack(10, 20) + combined = s + s2 + print(f"\nCombined: {combined}") + + print(f"\nPopping: {s.pop()}, {s.pop()}") + print(f"After pops: {s}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-22/solution.py b/src/content/day-22/solution.py new file mode 100644 index 0000000..67cec46 --- /dev/null +++ b/src/content/day-22/solution.py @@ -0,0 +1,111 @@ +""" +Day 22 Project: Custom Stack Data Structure +============================================= +A Stack class with complete dunder method support. +""" +from typing import Generic, TypeVar + +T = TypeVar("T") + + +class EmptyStackError(Exception): + """Raised when popping from an empty stack.""" + + +class Stack(Generic[T]): + """A LIFO stack data structure with full Python data model support. + + Supports: + len(), bool(), in, iteration, indexing, ==, +, repr(), str() + """ + + def __init__(self, *items: T) -> None: + self._data: list[T] = list(items) + + def push(self, item: T) -> None: + """Push an item onto the top of the stack.""" + self._data.append(item) + + def pop(self) -> T: + """Remove and return the top item.""" + if not self._data: + raise EmptyStackError("Cannot pop from an empty stack.") + return self._data.pop() + + def peek(self) -> T: + """Return the top item without removing it.""" + if not self._data: + raise EmptyStackError("Stack is empty.") + return self._data[-1] + + # --- Dunder Methods --- + + def __len__(self) -> int: + return len(self._data) + + def __bool__(self) -> bool: + return bool(self._data) + + def __contains__(self, item: object) -> bool: + return item in self._data + + def __iter__(self): + """Iterate from top to bottom.""" + return reversed(self._data) + + def __getitem__(self, index: int) -> T: + """Access by index from top (index 0 = top).""" + return self._data[-(index + 1)] + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Stack): + return NotImplemented + return self._data == other._data + + def __add__(self, other: "Stack[T]") -> "Stack[T]": + """Combine two stacks (other on top of self).""" + new = Stack(*self._data) + for item in other._data: + new.push(item) + return new + + def __repr__(self) -> str: + return f"Stack({self._data!r})" + + def __str__(self) -> str: + if not self._data: + return "Stack: [empty]" + items = " -> ".join(str(item) for item in reversed(self._data)) + return f"Stack: [{items}] (top ->)" + + +def main() -> None: + print("=" * 50) + print(" CUSTOM STACK DEMO") + print("=" * 50) + + s: Stack[int] = Stack(1, 2, 3) + s.push(4) + s.push(5) + + print(f"\nStack: {s}") + print(f"len(s): {len(s)}") + print(f"bool(s): {bool(s)}") + print(f"3 in s: {3 in s}") + print(f"s[0] (top): {s[0]}") + print(f"Peek: {s.peek()}") + + print("\nIterating (top to bottom):") + for item in s: + print(f" {item}") + + s2: Stack[int] = Stack(10, 20) + combined = s + s2 + print(f"\nCombined: {combined}") + + print(f"\nPopping: {s.pop()}, {s.pop()}") + print(f"After pops: {s}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-23/lesson.md b/src/content/day-23/lesson.md new file mode 100644 index 0000000..3d84e63 --- /dev/null +++ b/src/content/day-23/lesson.md @@ -0,0 +1,215 @@ +# Day 23 🎀 - Decorators + +--- + +## Overview + +Decorators modify or enhance functions without changing their source code. They are used everywhere in Python frameworks for caching, logging, authentication, and more. + +**What you will learn today:** + +- What a decorator is and how it works +- Writing your first decorator +- `@functools.wraps` to preserve metadata +- Decorators with arguments +- Stacking multiple decorators +- Real-world decorator patterns + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `decorator` | A function that takes another function and returns a modified version of it. Applied with the `@` syntax. | +| `@wraps` | From `functools`. Copies the original function's name, docstring, and metadata to the wrapper. Always use it. | +| `closure` | Decorators rely on closures: the inner `wrapper` function closes over the original `func`. | +| `parametrized decorator` | A decorator factory: a function that returns a decorator. Needs three levels of nesting. | + +--- + +## Code Examples + +### Your first decorator + +```python +import functools +import time + +def timer(func): + """Measure and print execution time of a function.""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + start = time.perf_counter() + result = func(*args, **kwargs) + elapsed = time.perf_counter() - start + print(f" [{func.__name__}] ran in {elapsed:.4f}s") + return result + return wrapper + +@timer +def slow_sum(n: int) -> int: + """Sum numbers from 0 to n.""" + return sum(range(n)) + +result = slow_sum(1_000_000) +print(result) # 499999500000 +# [slow_sum] ran in 0.0312s +``` + +### Decorators with arguments + +```python +import functools + +def repeat(times: int): + """Return a decorator that calls the function `times` times.""" + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + for _ in range(times): + result = func(*args, **kwargs) + return result + return wrapper + return decorator + +@repeat(times=3) +def greet(name: str) -> str: + print(f"Hello, {name}!") + return f"Hello, {name}!" + +greet("Alice") +# Hello, Alice! +# Hello, Alice! +# Hello, Alice! +``` + +--- + +## Today's Project: Function Profiler + +> Build a decorator toolkit with `@timer`, `@retry`, `@cache`, and `@log_calls` to profile and enhance functions. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 23 Project: Function Profiler Decorator Toolkit +===================================================== +A set of useful, production-quality decorators. +""" +import functools +import time +from typing import Callable, TypeVar + +F = TypeVar("F", bound=Callable) + +def timer(func: F) -> F: + """Measure and print the execution time of a function.""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + start = time.perf_counter() + result = func(*args, **kwargs) + elapsed = time.perf_counter() - start + print(f" [TIMER] {func.__name__}() -> {elapsed*1000:.3f}ms") + return result + return wrapper # type: ignore + +def retry(times: int = 3, exceptions: tuple = (Exception,), delay: float = 0.0): + """Retry a function up to `times` times on specified exceptions.""" + def decorator(func: F) -> F: + @functools.wraps(func) + def wrapper(*args, **kwargs): + last_exc = None + for attempt in range(1, times + 1): + try: + return func(*args, **kwargs) + except exceptions as e: + last_exc = e + print(f" [RETRY] {func.__name__}() attempt {attempt}/{times} failed: {e}") + if delay and attempt < times: + time.sleep(delay) + raise last_exc # type: ignore + return wrapper # type: ignore + return decorator + +def memoize(func: F) -> F: + """Cache function results for identical arguments.""" + cache: dict = {} + + @functools.wraps(func) + def wrapper(*args): + if args not in cache: + cache[args] = func(*args) + print(f" [CACHE MISS] {func.__name__}{args}") + else: + print(f" [CACHE HIT] {func.__name__}{args}") + return cache[args] + + wrapper.cache = cache # type: ignore + return wrapper # type: ignore + +def log_calls(func: F) -> F: + """Log every call to a function with its arguments and return value.""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + arg_str = ", ".join([repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]) + result = func(*args, **kwargs) + print(f" [LOG] {func.__name__}({arg_str}) -> {result!r}") + return result + return wrapper # type: ignore + +# --- Demo --- + +@timer +@log_calls +def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + +@memoize +def fibonacci(n: int) -> int: + """Compute the nth Fibonacci number recursively.""" + if n <= 1: + return n + return fibonacci(n - 1) + fibonacci(n - 2) + +import random + +@retry(times=3, exceptions=(ValueError,)) +def flaky_function() -> str: + """Randomly fails to simulate an unreliable operation.""" + if random.random() < 0.7: + raise ValueError("Random failure!") + return "Success!" + +def main() -> None: + print("=" * 55) + print(" DECORATOR TOOLKIT DEMO") + print("=" * 55) + + print("\n1. @timer + @log_calls (stacked):") + add(10, 20) + + print("\n2. @memoize (caching Fibonacci):") + for n in [5, 5, 7, 5]: + print(f" fibonacci({n}) = {fibonacci(n)}") + + print("\n3. @retry (unreliable function):") + try: + result = flaky_function() + print(f" Result: {result}") + except ValueError: + print(" All retries failed.") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-23/solution.py b/src/content/day-23/solution.py new file mode 100644 index 0000000..ac3df3f --- /dev/null +++ b/src/content/day-23/solution.py @@ -0,0 +1,120 @@ +""" +Day 23 Project: Function Profiler Decorator Toolkit +===================================================== +A set of useful, production-quality decorators. +""" +import functools +import time +from typing import Callable, TypeVar + +F = TypeVar("F", bound=Callable) + + +def timer(func: F) -> F: + """Measure and print the execution time of a function.""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + start = time.perf_counter() + result = func(*args, **kwargs) + elapsed = time.perf_counter() - start + print(f" [TIMER] {func.__name__}() -> {elapsed*1000:.3f}ms") + return result + return wrapper # type: ignore + + +def retry(times: int = 3, exceptions: tuple = (Exception,), delay: float = 0.0): + """Retry a function up to `times` times on specified exceptions.""" + def decorator(func: F) -> F: + @functools.wraps(func) + def wrapper(*args, **kwargs): + last_exc = None + for attempt in range(1, times + 1): + try: + return func(*args, **kwargs) + except exceptions as e: + last_exc = e + print(f" [RETRY] {func.__name__}() attempt {attempt}/{times} failed: {e}") + if delay and attempt < times: + time.sleep(delay) + raise last_exc # type: ignore + return wrapper # type: ignore + return decorator + + +def memoize(func: F) -> F: + """Cache function results for identical arguments.""" + cache: dict = {} + + @functools.wraps(func) + def wrapper(*args): + if args not in cache: + cache[args] = func(*args) + print(f" [CACHE MISS] {func.__name__}{args}") + else: + print(f" [CACHE HIT] {func.__name__}{args}") + return cache[args] + + wrapper.cache = cache # type: ignore + return wrapper # type: ignore + + +def log_calls(func: F) -> F: + """Log every call to a function with its arguments and return value.""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + arg_str = ", ".join([repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]) + result = func(*args, **kwargs) + print(f" [LOG] {func.__name__}({arg_str}) -> {result!r}") + return result + return wrapper # type: ignore + + +# --- Demo --- + +@timer +@log_calls +def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + + +@memoize +def fibonacci(n: int) -> int: + """Compute the nth Fibonacci number recursively.""" + if n <= 1: + return n + return fibonacci(n - 1) + fibonacci(n - 2) + + +import random + +@retry(times=3, exceptions=(ValueError,)) +def flaky_function() -> str: + """Randomly fails to simulate an unreliable operation.""" + if random.random() < 0.7: + raise ValueError("Random failure!") + return "Success!" + + +def main() -> None: + print("=" * 55) + print(" DECORATOR TOOLKIT DEMO") + print("=" * 55) + + print("\n1. @timer + @log_calls (stacked):") + add(10, 20) + + print("\n2. @memoize (caching Fibonacci):") + for n in [5, 5, 7, 5]: + print(f" fibonacci({n}) = {fibonacci(n)}") + + print("\n3. @retry (unreliable function):") + try: + result = flaky_function() + print(f" Result: {result}") + except ValueError: + print(" All retries failed.") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-24/lesson.md b/src/content/day-24/lesson.md new file mode 100644 index 0000000..18d4878 --- /dev/null +++ b/src/content/day-24/lesson.md @@ -0,0 +1,183 @@ +# Day 24 ♾️ - Generators & Iterators + +--- + +## Overview + +Generators produce values lazily - one at a time - enabling memory-efficient processing of large or infinite sequences. + +**What you will learn today:** + +- `yield` to create generator functions +- Generator expressions `(x for x in ...)` +- The iterator protocol: `__iter__` and `__next__` +- `next()` and `StopIteration` +- Infinite generators +- `itertools` module for powerful iteration patterns + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `yield` | Pauses function execution and returns a value. Resumes from the same point when `next()` is called again. | +| `generator` | A function containing `yield` returns a generator object. It is lazy: values are computed only when requested. | +| `generator expression` | Like a list comprehension but lazy: `(x**2 for x in range(1000000))` uses almost no memory. | +| `itertools` | Standard library module with powerful iteration tools: `chain()`, `islice()`, `product()`, `cycle()`, etc. | + +--- + +## Code Examples + +### Generator functions + +```python +def countdown(n: int): + """Generate numbers from n down to 1.""" + while n > 0: + yield n + n -= 1 + +# The generator does not run until iterated +gen = countdown(5) +print(next(gen)) # 5 +print(next(gen)) # 4 + +# Iterate over it +for num in countdown(3): + print(num) # 3, 2, 1 + +# Generator vs list: memory comparison +import sys +list_sq = [x**2 for x in range(1000)] +gen_sq = (x**2 for x in range(1000)) +print(sys.getsizeof(list_sq)) # ~8856 bytes +print(sys.getsizeof(gen_sq)) # 104 bytes +``` + +### Infinite generators and itertools + +```python +import itertools + +def fibonacci(): + """Generate infinite Fibonacci sequence.""" + a, b = 0, 1 + while True: + yield a + a, b = b, a + b + +# Take only first 10 values from an infinite generator +first_10 = list(itertools.islice(fibonacci(), 10)) +print(first_10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] + +# itertools.count: infinite counter +counter = itertools.count(start=1, step=2) +odd_numbers = list(itertools.islice(counter, 5)) +print(odd_numbers) # [1, 3, 5, 7, 9] + +# itertools.chain: combine iterables +combined = list(itertools.chain([1, 2], [3, 4], [5, 6])) +print(combined) # [1, 2, 3, 4, 5, 6] +``` + +--- + +## Today's Project: Infinite Sequences Generator Library + +> Build a library of generator functions for mathematical sequences: Fibonacci, primes, collatz, and more. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 24 Project: Infinite Sequences Generator Library +====================================================== +Memory-efficient infinite mathematical sequences. +""" +import itertools +from typing import Generator + +def fibonacci() -> Generator[int, None, None]: + """Yield the infinite Fibonacci sequence: 0, 1, 1, 2, 3, 5, ...""" + a, b = 0, 1 + while True: + yield a + a, b = b, a + b + +def primes() -> Generator[int, None, None]: + """Yield all prime numbers: 2, 3, 5, 7, 11, ...""" + sieve: dict[int, list[int]] = {} + n = 2 + while True: + if n not in sieve: + yield n + sieve[n * n] = [n] + else: + for p in sieve[n]: + sieve.setdefault(n + p, []).append(p) + del sieve[n] + n += 1 + +def collatz(n: int) -> Generator[int, None, None]: + """Yield the Collatz sequence starting at n.""" + while n != 1: + yield n + n = n // 2 if n % 2 == 0 else 3 * n + 1 + yield 1 + +def triangular() -> Generator[int, None, None]: + """Yield triangular numbers: 1, 3, 6, 10, 15, ...""" + n, total = 1, 0 + while True: + total += n + yield total + n += 1 + +def running_average(iterable) -> Generator[float, None, None]: + """Yield the running average of values from an iterable.""" + total = 0.0 + for count, value in enumerate(iterable, start=1): + total += value + yield total / count + +def take(n: int, gen) -> list: + """Return the first n values from a generator.""" + return list(itertools.islice(gen, n)) + +def main() -> None: + print("=" * 55) + print(" INFINITE SEQUENCES GENERATOR LIBRARY") + print("=" * 55) + + print(f"\nFirst 10 Fibonacci : {take(10, fibonacci())}") + print(f"First 10 Primes : {take(10, primes())}") + print(f"First 10 Triangular : {take(10, triangular())}") + + n = 27 + col = take(100, collatz(n)) + print(f"\nCollatz({n}) - {len(col)} steps:") + print(f" {col[:15]}...") + print(f" Max value in sequence: {max(col)}") + + data = [4, 8, 15, 16, 23, 42] + avgs = list(running_average(data)) + print(f"\nRunning average of {data}:") + print(f" {[round(a, 2) for a in avgs]}") + + print(f"\nPrimes between 50 and 100:") + big_primes = list(itertools.takewhile(lambda x: x <= 100, itertools.dropwhile(lambda x: x < 50, primes()))) + print(f" {big_primes}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-24/solution.py b/src/content/day-24/solution.py new file mode 100644 index 0000000..d92a842 --- /dev/null +++ b/src/content/day-24/solution.py @@ -0,0 +1,89 @@ +""" +Day 24 Project: Infinite Sequences Generator Library +====================================================== +Memory-efficient infinite mathematical sequences. +""" +import itertools +from typing import Generator + + +def fibonacci() -> Generator[int, None, None]: + """Yield the infinite Fibonacci sequence: 0, 1, 1, 2, 3, 5, ...""" + a, b = 0, 1 + while True: + yield a + a, b = b, a + b + + +def primes() -> Generator[int, None, None]: + """Yield all prime numbers: 2, 3, 5, 7, 11, ...""" + sieve: dict[int, list[int]] = {} + n = 2 + while True: + if n not in sieve: + yield n + sieve[n * n] = [n] + else: + for p in sieve[n]: + sieve.setdefault(n + p, []).append(p) + del sieve[n] + n += 1 + + +def collatz(n: int) -> Generator[int, None, None]: + """Yield the Collatz sequence starting at n.""" + while n != 1: + yield n + n = n // 2 if n % 2 == 0 else 3 * n + 1 + yield 1 + + +def triangular() -> Generator[int, None, None]: + """Yield triangular numbers: 1, 3, 6, 10, 15, ...""" + n, total = 1, 0 + while True: + total += n + yield total + n += 1 + + +def running_average(iterable) -> Generator[float, None, None]: + """Yield the running average of values from an iterable.""" + total = 0.0 + for count, value in enumerate(iterable, start=1): + total += value + yield total / count + + +def take(n: int, gen) -> list: + """Return the first n values from a generator.""" + return list(itertools.islice(gen, n)) + + +def main() -> None: + print("=" * 55) + print(" INFINITE SEQUENCES GENERATOR LIBRARY") + print("=" * 55) + + print(f"\nFirst 10 Fibonacci : {take(10, fibonacci())}") + print(f"First 10 Primes : {take(10, primes())}") + print(f"First 10 Triangular : {take(10, triangular())}") + + n = 27 + col = take(100, collatz(n)) + print(f"\nCollatz({n}) - {len(col)} steps:") + print(f" {col[:15]}...") + print(f" Max value in sequence: {max(col)}") + + data = [4, 8, 15, 16, 23, 42] + avgs = list(running_average(data)) + print(f"\nRunning average of {data}:") + print(f" {[round(a, 2) for a in avgs]}") + + print(f"\nPrimes between 50 and 100:") + big_primes = list(itertools.takewhile(lambda x: x <= 100, itertools.dropwhile(lambda x: x < 50, primes()))) + print(f" {big_primes}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-25/lesson.md b/src/content/day-25/lesson.md new file mode 100644 index 0000000..9aec715 --- /dev/null +++ b/src/content/day-25/lesson.md @@ -0,0 +1,198 @@ +# Day 25 🔒 - Context Managers + +--- + +## Overview + +Context managers ensure resources are properly acquired and released - even when errors occur. The `with` statement is Python's way of making this automatic. + +**What you will learn today:** + +- The `with` statement and why it exists +- How `__enter__` and `__exit__` work +- Using `contextlib.contextmanager` with `yield` +- Writing reusable context managers +- Nested context managers +- Common use cases: files, locks, timing, temp directories + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `with` | Guarantees cleanup code runs, even if an exception occurs inside the block. | +| `__enter__` | Called when entering the `with` block. The return value is bound to the `as` variable. | +| `__exit__` | Called when leaving the block, receiving exception info if one occurred. Return `True` to suppress the exception. | +| `@contextmanager` | Decorator from `contextlib` that turns a generator function into a context manager. Much simpler than a class. | + +--- + +## Code Examples + +### Class-based context manager + +```python +class Timer: + """Context manager that measures elapsed time.""" + import time + + def __enter__(self): + import time + self._start = time.perf_counter() + return self # bound to the `as` variable + + def __exit__(self, exc_type, exc_val, exc_tb): + import time + self.elapsed = time.perf_counter() - self._start + print(f"Elapsed: {self.elapsed:.4f}s") + return False # do not suppress exceptions + +with Timer() as t: + total = sum(range(1_000_000)) + +print(f"Result: {total}") +print(f"Time: {t.elapsed:.4f}s") +``` + +### contextlib.contextmanager + +```python +from contextlib import contextmanager +import os +import tempfile + +@contextmanager +def temp_directory(): + """Create a temporary directory and clean it up afterward.""" + import tempfile + import shutil + tmpdir = tempfile.mkdtemp() + print(f"Created temp dir: {tmpdir}") + try: + yield tmpdir # pause and hand control to the `with` block + finally: + shutil.rmtree(tmpdir) # always runs + print(f"Cleaned up: {tmpdir}") + +with temp_directory() as tmpdir: + # Create files in the temp directory + filepath = os.path.join(tmpdir, "test.txt") + with open(filepath, "w") as f: + f.write("Temporary data") + print(f"File exists: {os.path.exists(filepath)}") + +print(f"After block - dir gone: {not os.path.exists(tmpdir)}") +``` + +--- + +## Today's Project: File Manager Context Manager + +> Build a collection of context managers for safe file operations, atomic writes, and managed working directories. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 25 Project: File Manager Context Managers +================================================ +Safe file operations using context managers. +""" +import os +import shutil +import tempfile +import time +from contextlib import contextmanager +from pathlib import Path + +class SafeWriter: + """Write to a file atomically: write to temp file, then rename. + + This prevents partial writes from corrupting the target file. + """ + + def __init__(self, path: str | Path) -> None: + self.path = Path(path) + self._tmp_path: Path | None = None + self._file = None + + def __enter__(self): + fd, tmp = tempfile.mkstemp(dir=self.path.parent, prefix=".tmp_") + os.close(fd) + self._tmp_path = Path(tmp) + self._file = open(self._tmp_path, "w", encoding="utf-8") + return self._file + + def __exit__(self, exc_type, exc_val, exc_tb): + self._file.close() + if exc_type is None: + # Success: replace target with temp file + self._tmp_path.replace(self.path) + print(f" [SafeWriter] Committed -> {self.path}") + else: + # Error: delete temp file, do not touch original + self._tmp_path.unlink(missing_ok=True) + print(f" [SafeWriter] Rolled back (exception: {exc_type.__name__})") + return False # do not suppress exceptions + +@contextmanager +def working_directory(path: str | Path): + """Temporarily change the working directory.""" + original = Path.cwd() + os.chdir(path) + print(f" [CD] -> {path}") + try: + yield Path(path) + finally: + os.chdir(original) + print(f" [CD] <- {original}") + +@contextmanager +def timer(label: str = ""): + """Measure execution time of a block.""" + start = time.perf_counter() + try: + yield + finally: + elapsed = time.perf_counter() - start + tag = f"[{label}] " if label else "" + print(f" {tag}elapsed: {elapsed*1000:.2f}ms") + +def main() -> None: + print("=" * 55) + print(" FILE MANAGER DEMO") + print("=" * 55) + + # Demo 1: Safe atomic write + print("\n1. SafeWriter (atomic write):") + with SafeWriter("output.txt") as f: + f.write("Line 1\nLine 2\nLine 3\n") + print(f" File content: {Path('output.txt').read_text().strip()!r}") + Path("output.txt").unlink(missing_ok=True) + + # Demo 2: working_directory + print("\n2. working_directory context manager:") + with tempfile.TemporaryDirectory() as tmpdir: + with working_directory(tmpdir): + print(f" Inside: {Path.cwd()}") + Path("local.txt").write_text("hello") + print(f" Back to: {Path.cwd()}") + + # Demo 3: timer + print("\n3. timer context manager:") + with timer("sum 1M"): + result = sum(range(1_000_000)) + print(f" Result: {result}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-25/solution.py b/src/content/day-25/solution.py new file mode 100644 index 0000000..abfa863 --- /dev/null +++ b/src/content/day-25/solution.py @@ -0,0 +1,98 @@ +""" +Day 25 Project: File Manager Context Managers +================================================ +Safe file operations using context managers. +""" +import os +import shutil +import tempfile +import time +from contextlib import contextmanager +from pathlib import Path + + +class SafeWriter: + """Write to a file atomically: write to temp file, then rename. + + This prevents partial writes from corrupting the target file. + """ + + def __init__(self, path: str | Path) -> None: + self.path = Path(path) + self._tmp_path: Path | None = None + self._file = None + + def __enter__(self): + fd, tmp = tempfile.mkstemp(dir=self.path.parent, prefix=".tmp_") + os.close(fd) + self._tmp_path = Path(tmp) + self._file = open(self._tmp_path, "w", encoding="utf-8") + return self._file + + def __exit__(self, exc_type, exc_val, exc_tb): + self._file.close() + if exc_type is None: + # Success: replace target with temp file + self._tmp_path.replace(self.path) + print(f" [SafeWriter] Committed -> {self.path}") + else: + # Error: delete temp file, do not touch original + self._tmp_path.unlink(missing_ok=True) + print(f" [SafeWriter] Rolled back (exception: {exc_type.__name__})") + return False # do not suppress exceptions + + +@contextmanager +def working_directory(path: str | Path): + """Temporarily change the working directory.""" + original = Path.cwd() + os.chdir(path) + print(f" [CD] -> {path}") + try: + yield Path(path) + finally: + os.chdir(original) + print(f" [CD] <- {original}") + + +@contextmanager +def timer(label: str = ""): + """Measure execution time of a block.""" + start = time.perf_counter() + try: + yield + finally: + elapsed = time.perf_counter() - start + tag = f"[{label}] " if label else "" + print(f" {tag}elapsed: {elapsed*1000:.2f}ms") + + +def main() -> None: + print("=" * 55) + print(" FILE MANAGER DEMO") + print("=" * 55) + + # Demo 1: Safe atomic write + print("\n1. SafeWriter (atomic write):") + with SafeWriter("output.txt") as f: + f.write("Line 1\nLine 2\nLine 3\n") + print(f" File content: {Path('output.txt').read_text().strip()!r}") + Path("output.txt").unlink(missing_ok=True) + + # Demo 2: working_directory + print("\n2. working_directory context manager:") + with tempfile.TemporaryDirectory() as tmpdir: + with working_directory(tmpdir): + print(f" Inside: {Path.cwd()}") + Path("local.txt").write_text("hello") + print(f" Back to: {Path.cwd()}") + + # Demo 3: timer + print("\n3. timer context manager:") + with timer("sum 1M"): + result = sum(range(1_000_000)) + print(f" Result: {result}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-26/lesson.md b/src/content/day-26/lesson.md new file mode 100644 index 0000000..7eae9a8 --- /dev/null +++ b/src/content/day-26/lesson.md @@ -0,0 +1,163 @@ +# Day 26 🔍 - Regular Expressions + +--- + +## Overview + +Regular expressions are a mini language for matching patterns in text. Master them and you can parse, validate, and transform any text data. + +**What you will learn today:** + +- The `re` module: `re.match()`, `re.search()`, `re.findall()`, `re.sub()` +- Character classes: `\d`, `\w`, `\s`, `[abc]` +- Quantifiers: `*`, `+`, `?`, `{n,m}` +- Groups `()` and named groups `(?P...)` +- Flags: `re.IGNORECASE`, `re.MULTILINE` +- Compiling patterns with `re.compile()` + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `re.search()` | Search anywhere in the string for the pattern. Returns a Match object or None. | +| `re.findall()` | Return all non-overlapping matches as a list of strings. | +| `re.sub()` | Replace all matches with a replacement string. | +| `re.compile()` | Pre-compile a pattern for reuse, more efficient when the same pattern is used many times. | + +--- + +## Code Examples + +### Core regex patterns + +```python +import re + +text = "Alice's email is alice@example.com and her phone is 415-555-1234" + +# Search anywhere in the string +match = re.search(r"\d{3}-\d{3}-\d{4}", text) +if match: + print(match.group()) # 415-555-1234 + +# Find all email addresses +emails = re.findall(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", text) +print(emails) # ['alice@example.com'] + +# Named groups +pattern = re.compile(r"(?P\d{3})-(?P\d{3})-(?P\d{4})") +m = pattern.search(text) +if m: + print(m.group("area")) # 415 + print(m.group("exchange")) # 555 +``` + +### re.sub and flags + +```python +import re + +# Remove all punctuation +text = "Hello, World! How are you?" +clean = re.sub(r"[^\w\s]", "", text) +print(clean) # Hello World How are you + +# IGNORECASE flag +sentence = "Python is great. PYTHON rocks. python wins." +count = len(re.findall(r"python", sentence, re.IGNORECASE)) +print(count) # 3 + +# MULTILINE: ^ matches start of each line +multiline = "first\nsecond\nthird" +starts = re.findall(r"^\w+", multiline, re.MULTILINE) +print(starts) # ['first', 'second', 'third'] +``` + +--- + +## Today's Project: Text Parser + +> Extract emails, phone numbers, URLs, and names from raw text using compiled regex patterns. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 26 Project: Text Parser +============================= +Extract structured data from unstructured text using regex. +""" +import re + +EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}") +PHONE_RE = re.compile(r"\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b") +URL_RE = re.compile(r"https?://(?:www\.)?[-\w@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-\w()@:%_+.~#?&/=]*") +NAME_RE = re.compile(r"\b([A-Z][a-z]+ [A-Z][a-z]+)\b") + +def extract_all(text: str) -> dict[str, list[str]]: + """Extract emails, phones, URLs, and names from text.""" + return { + "emails": EMAIL_RE.findall(text), + "phones": PHONE_RE.findall(text), + "urls": URL_RE.findall(text), + "names": NAME_RE.findall(text), + } + +def mask_email(email: str) -> str: + """Mask an email: al***@example.com""" + local, domain = email.split("@", 1) + return local[:2] + "***@" + domain + +def validate_email(email: str) -> bool: + """Return True if email looks valid.""" + return bool(EMAIL_RE.fullmatch(email)) + +SAMPLE = """ +Contact us at support@example.com or sales@company.org. +Reach Alice Johnson at 415-555-1234 or (800) 555-9876. +Visit https://www.example.com or http://docs.example.com. +Email Bob Smith at press@newsroom.io for press inquiries. +""" + +def main() -> None: + print("=" * 55) + print(" TEXT PARSER") + print("=" * 55) + + results = extract_all(SAMPLE) + print(f"\nEmails : {results['emails']}") + print(f"Phones : {results['phones']}") + print(f"URLs : {results['urls']}") + print(f"Names : {results['names']}") + + print("\nMasked emails:") + for email in results["emails"]: + print(f" {email} -> {mask_email(email)}") + + print("\nEmail validation:") + tests = ["valid@example.com", "not-an-email", "also@valid.org", "@missing.com"] + for t in tests: + status = "VALID" if validate_email(t) else "INVALID" + print(f" {t:<28} {status}") + + pattern_str = input("\nEnter a regex pattern to test (or press Enter to skip): ").strip() + if pattern_str: + try: + matches = re.findall(pattern_str, SAMPLE) + print(f" Matches: {matches}") + except re.error as e: + print(f" Invalid pattern: {e}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-26/solution.py b/src/content/day-26/solution.py new file mode 100644 index 0000000..2fd9cd5 --- /dev/null +++ b/src/content/day-26/solution.py @@ -0,0 +1,75 @@ +""" +Day 26 Project: Text Parser +============================= +Extract structured data from unstructured text using regex. +""" +import re + + +EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}") +PHONE_RE = re.compile(r"\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b") +URL_RE = re.compile(r"https?://(?:www\.)?[-\w@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-\w()@:%_+.~#?&/=]*") +NAME_RE = re.compile(r"\b([A-Z][a-z]+ [A-Z][a-z]+)\b") + + +def extract_all(text: str) -> dict[str, list[str]]: + """Extract emails, phones, URLs, and names from text.""" + return { + "emails": EMAIL_RE.findall(text), + "phones": PHONE_RE.findall(text), + "urls": URL_RE.findall(text), + "names": NAME_RE.findall(text), + } + + +def mask_email(email: str) -> str: + """Mask an email: al***@example.com""" + local, domain = email.split("@", 1) + return local[:2] + "***@" + domain + + +def validate_email(email: str) -> bool: + """Return True if email looks valid.""" + return bool(EMAIL_RE.fullmatch(email)) + + +SAMPLE = """ +Contact us at support@example.com or sales@company.org. +Reach Alice Johnson at 415-555-1234 or (800) 555-9876. +Visit https://www.example.com or http://docs.example.com. +Email Bob Smith at press@newsroom.io for press inquiries. +""" + + +def main() -> None: + print("=" * 55) + print(" TEXT PARSER") + print("=" * 55) + + results = extract_all(SAMPLE) + print(f"\nEmails : {results['emails']}") + print(f"Phones : {results['phones']}") + print(f"URLs : {results['urls']}") + print(f"Names : {results['names']}") + + print("\nMasked emails:") + for email in results["emails"]: + print(f" {email} -> {mask_email(email)}") + + print("\nEmail validation:") + tests = ["valid@example.com", "not-an-email", "also@valid.org", "@missing.com"] + for t in tests: + status = "VALID" if validate_email(t) else "INVALID" + print(f" {t:<28} {status}") + + pattern_str = input("\nEnter a regex pattern to test (or press Enter to skip): ").strip() + if pattern_str: + try: + matches = re.findall(pattern_str, SAMPLE) + print(f" Matches: {matches}") + except re.error as e: + print(f" Invalid pattern: {e}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-27/lesson.md b/src/content/day-27/lesson.md new file mode 100644 index 0000000..494ae20 --- /dev/null +++ b/src/content/day-27/lesson.md @@ -0,0 +1,180 @@ +# Day 27 📊 - JSON & CSV Data + +--- + +## Overview + +JSON and CSV are the two most common data formats you will encounter. Learn to read, write, and process both with Python's standard library. + +**What you will learn today:** + +- Reading and writing JSON with the `json` module +- Pretty-printing JSON with `indent=2` +- Reading CSV with `csv.DictReader` +- Writing CSV with `csv.DictWriter` +- Using `pathlib` for clean file handling +- Converting between JSON and Python objects + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `json.dumps()` | Serialize a Python object to a JSON string. Use `indent=2` for readable output. | +| `json.loads()` | Parse a JSON string into a Python object. | +| `json.load() / json.dump()` | Read/write JSON directly to/from a file object. | +| `csv.DictReader` | Read CSV rows as dicts keyed by column header. | +| `csv.DictWriter` | Write dicts to CSV. Specify `fieldnames` to control column order. | + +--- + +## Code Examples + +### JSON read and write + +```python +import json +from pathlib import Path + +data = { + "name": "Alice", + "scores": [95, 87, 92], + "active": True, + "address": None, +} + +# Python -> JSON string +json_str = json.dumps(data, indent=2) +print(json_str) + +# JSON string -> Python +parsed = json.loads(json_str) +print(parsed["name"]) # Alice + +# Write to file +Path("data.json").write_text(json.dumps(data, indent=2)) + +# Read from file +with open("data.json") as f: + loaded = json.load(f) +print(loaded) +``` + +### CSV read and write + +```python +import csv + +students = [ + {"name": "Alice", "grade": 92, "subject": "Math"}, + {"name": "Bob", "grade": 78, "subject": "Science"}, +] + +# Write CSV +with open("students.csv", "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=["name", "grade", "subject"]) + writer.writeheader() + writer.writerows(students) + +# Read CSV +with open("students.csv", "r") as f: + reader = csv.DictReader(f) + for row in reader: + print(f"{row['name']}: {row['grade']} in {row['subject']}") +``` + +--- + +## Today's Project: Grade Book + +> Build a grade book that loads CSV data, computes statistics, exports to JSON, and prints a formatted report. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 27 Project: Grade Book +=========================== +Read CSV grades, compute stats, export JSON, print report. +""" +import csv +import json +import statistics +from pathlib import Path + +SAMPLE_CSV = """name,math,science,english,history +Alice,92,88,95,90 +Bob,78,82,74,85 +Charlie,65,70,80,72 +Diana,95,98,92,96 +Eve,55,60,58,62 +Frank,88,75,82,79 +""" + +CSV_FILE = Path("grades.csv") +JSON_FILE = Path("grades.json") + +def letter_grade(avg: float) -> str: + if avg >= 90: return "A" + if avg >= 80: return "B" + if avg >= 70: return "C" + if avg >= 60: return "D" + return "F" + +def load_grades(path: Path) -> list[dict]: + with open(path, newline="") as f: + reader = csv.DictReader(f) + records = [] + for row in reader: + name = row.pop("name") + grades = {k: int(v) for k, v in row.items()} + average = round(sum(grades.values()) / len(grades), 1) + records.append({"name": name, "grades": grades, + "average": average, "letter": letter_grade(average)}) + return records + +def print_report(records: list[dict]) -> None: + subjects = list(records[0]["grades"].keys()) + w = 10 + print("\n" + "=" * 70) + print(" GRADE REPORT") + print("=" * 70) + hdr = f" {'Name':<12}" + "".join(f"{s.capitalize():>{w}}" for s in subjects) + f" {'Avg':>6} Grade" + print(hdr) + print(" " + "-" * 68) + for r in sorted(records, key=lambda x: x["average"], reverse=True): + cols = "".join(f"{r['grades'][s]:>{w}}" for s in subjects) + print(f" {r['name']:<12}{cols} {r['average']:>6.1f} {r['letter']}") + print(" " + "-" * 68) + print("\n Subject Averages:") + for subj in subjects: + vals = [r["grades"][subj] for r in records] + print(f" {subj.capitalize():<12} avg={statistics.mean(vals):.1f} " + f"min={min(vals)} max={max(vals)} stdev={statistics.stdev(vals):.1f}") + all_avgs = [r["average"] for r in records] + top = max(records, key=lambda x: x["average"]) + print(f"\n Class average : {statistics.mean(all_avgs):.1f}") + print(f" Top student : {top['name']} ({top['average']}%)") + print("=" * 70) + +def main() -> None: + CSV_FILE.write_text(SAMPLE_CSV) + records = load_grades(CSV_FILE) + print_report(records) + JSON_FILE.write_text(json.dumps(records, indent=2)) + print(f"\n Exported to {JSON_FILE}") + CSV_FILE.unlink(missing_ok=True) + JSON_FILE.unlink(missing_ok=True) + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-27/solution.py b/src/content/day-27/solution.py new file mode 100644 index 0000000..f243426 --- /dev/null +++ b/src/content/day-27/solution.py @@ -0,0 +1,81 @@ +""" +Day 27 Project: Grade Book +=========================== +Read CSV grades, compute stats, export JSON, print report. +""" +import csv +import json +import statistics +from pathlib import Path + +SAMPLE_CSV = """name,math,science,english,history +Alice,92,88,95,90 +Bob,78,82,74,85 +Charlie,65,70,80,72 +Diana,95,98,92,96 +Eve,55,60,58,62 +Frank,88,75,82,79 +""" + +CSV_FILE = Path("grades.csv") +JSON_FILE = Path("grades.json") + + +def letter_grade(avg: float) -> str: + if avg >= 90: return "A" + if avg >= 80: return "B" + if avg >= 70: return "C" + if avg >= 60: return "D" + return "F" + + +def load_grades(path: Path) -> list[dict]: + with open(path, newline="") as f: + reader = csv.DictReader(f) + records = [] + for row in reader: + name = row.pop("name") + grades = {k: int(v) for k, v in row.items()} + average = round(sum(grades.values()) / len(grades), 1) + records.append({"name": name, "grades": grades, + "average": average, "letter": letter_grade(average)}) + return records + + +def print_report(records: list[dict]) -> None: + subjects = list(records[0]["grades"].keys()) + w = 10 + print("\n" + "=" * 70) + print(" GRADE REPORT") + print("=" * 70) + hdr = f" {'Name':<12}" + "".join(f"{s.capitalize():>{w}}" for s in subjects) + f" {'Avg':>6} Grade" + print(hdr) + print(" " + "-" * 68) + for r in sorted(records, key=lambda x: x["average"], reverse=True): + cols = "".join(f"{r['grades'][s]:>{w}}" for s in subjects) + print(f" {r['name']:<12}{cols} {r['average']:>6.1f} {r['letter']}") + print(" " + "-" * 68) + print("\n Subject Averages:") + for subj in subjects: + vals = [r["grades"][subj] for r in records] + print(f" {subj.capitalize():<12} avg={statistics.mean(vals):.1f} " + f"min={min(vals)} max={max(vals)} stdev={statistics.stdev(vals):.1f}") + all_avgs = [r["average"] for r in records] + top = max(records, key=lambda x: x["average"]) + print(f"\n Class average : {statistics.mean(all_avgs):.1f}") + print(f" Top student : {top['name']} ({top['average']}%)") + print("=" * 70) + + +def main() -> None: + CSV_FILE.write_text(SAMPLE_CSV) + records = load_grades(CSV_FILE) + print_report(records) + JSON_FILE.write_text(json.dumps(records, indent=2)) + print(f"\n Exported to {JSON_FILE}") + CSV_FILE.unlink(missing_ok=True) + JSON_FILE.unlink(missing_ok=True) + + +if __name__ == "__main__": + main() diff --git a/src/content/day-28/lesson.md b/src/content/day-28/lesson.md new file mode 100644 index 0000000..f2679bc --- /dev/null +++ b/src/content/day-28/lesson.md @@ -0,0 +1,171 @@ +# Day 28 🌐 - Working with APIs + +--- + +## Overview + +Connect your Python programs to the internet by calling REST APIs. This skill unlocks weather apps, AI integrations, data feeds, and much more. + +**What you will learn today:** + +- What a REST API is and how HTTP works +- Making GET requests with `urllib.request` +- Parsing JSON responses from APIs +- Handling HTTP errors and timeouts +- Query parameters with `urllib.parse` +- API keys and environment variables + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `REST API` | A web service that responds to HTTP requests with structured data, usually JSON. | +| `requests.get()` | Send an HTTP GET request. Returns a Response with `.status_code`, `.json()`, `.text`. | +| `status_code` | 200=OK, 404=Not Found, 429=Rate Limited, 500=Server Error. | +| `raise_for_status()` | Raises `HTTPError` on 4xx/5xx responses. Call it after every request. | + +--- + +## Code Examples + +### Making your first API call + +```python +import urllib.request +import json + +# Using the built-in urllib (no install required) +url = "https://api.chucknorris.io/jokes/random" +with urllib.request.urlopen(url) as response: + data = json.loads(response.read().decode("utf-8")) +print(data["value"]) + +# Using requests (pip install requests) +# import requests +# resp = requests.get(url, timeout=10) +# resp.raise_for_status() +# print(resp.json()["value"]) +``` + +### Error handling + +```python +import urllib.request +import urllib.error +import json + +def fetch_json(url: str) -> dict | list: + """Fetch JSON from a URL with proper error handling.""" + try: + with urllib.request.urlopen(url, timeout=10) as resp: + return json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as e: + raise RuntimeError(f"HTTP {e.code}: {e.reason}") from e + except urllib.error.URLError as e: + raise RuntimeError(f"Network error: {e.reason}") from e + +data = fetch_json("https://api.chucknorris.io/jokes/random") +print(data.get("value", "No joke found")) +``` + +--- + +## Today's Project: Weather CLI App + +> Fetch live weather for any city using the Open-Meteo API (completely free, no API key required). + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 28 Project: Weather CLI App +================================ +Fetch live weather using Open-Meteo API (no API key needed). +""" +import json +import urllib.request +import urllib.parse +import urllib.error + +WMO_CODES: dict[int, str] = { + 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", + 45: "Fog", 48: "Rime fog", 51: "Light drizzle", 53: "Moderate drizzle", + 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", + 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow", + 80: "Rain showers", 81: "Moderate showers", 82: "Violent showers", + 95: "Thunderstorm", 96: "Thunderstorm with hail", +} + +def geocode(city: str) -> tuple[float, float, str]: + """Return (lat, lon, display_name) for a city.""" + params = urllib.parse.urlencode({"name": city, "count": 1, "format": "json"}) + url = f"https://geocoding-api.open-meteo.com/v1/search?{params}" + with urllib.request.urlopen(url, timeout=10) as resp: + data = json.loads(resp.read()) + results = data.get("results", []) + if not results: + raise ValueError(f"City not found: {city!r}") + r = results[0] + return r["latitude"], r["longitude"], f"{r['name']}, {r.get('country', '')}" + +def get_weather(lat: float, lon: float) -> dict: + """Fetch current weather from Open-Meteo.""" + params = urllib.parse.urlencode({ + "latitude": lat, "longitude": lon, + "current_weather": "true", + "timezone": "auto", + "forecast_days": 1, + }) + url = f"https://api.open-meteo.com/v1/forecast?{params}" + with urllib.request.urlopen(url, timeout=10) as resp: + return json.loads(resp.read()) + +def display_weather(city_name: str, data: dict) -> None: + cw = data["current_weather"] + temp_c = cw["temperature"] + temp_f = temp_c * 9 / 5 + 32 + wind = cw["windspeed"] + code = int(cw["weathercode"]) + condition = WMO_CODES.get(code, "Unknown") + is_day = cw.get("is_day", 1) + icon = "☀️" if is_day and code == 0 else "⛈️" if code >= 95 else "🌧️" if 60 <= code < 70 else "🌤️" + + print(f""" + ╔══════════════════════════════════════╗ + {icon} {city_name} + ╠══════════════════════════════════════╣ + Condition : {condition} + Temperature : {temp_c}°C / {temp_f:.1f}°F + Wind Speed : {wind} km/h + ╚══════════════════════════════════════╝""") + +def main() -> None: + print("=" * 45) + print(" WEATHER CLI (Open-Meteo)") + print("=" * 45) + while True: + city = input("\nCity name (or 'quit'): ").strip() + if city.lower() in ("quit", "q", ""): + break + try: + lat, lon, name = geocode(city) + weather = get_weather(lat, lon) + display_weather(name, weather) + except ValueError as e: + print(f" {e}") + except Exception as e: + print(f" Network error: {e}") + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-28/solution.py b/src/content/day-28/solution.py new file mode 100644 index 0000000..1902529 --- /dev/null +++ b/src/content/day-28/solution.py @@ -0,0 +1,87 @@ +""" +Day 28 Project: Weather CLI App +================================ +Fetch live weather using Open-Meteo API (no API key needed). +""" +import json +import urllib.request +import urllib.parse +import urllib.error + + +WMO_CODES: dict[int, str] = { + 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", + 45: "Fog", 48: "Rime fog", 51: "Light drizzle", 53: "Moderate drizzle", + 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", + 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow", + 80: "Rain showers", 81: "Moderate showers", 82: "Violent showers", + 95: "Thunderstorm", 96: "Thunderstorm with hail", +} + + +def geocode(city: str) -> tuple[float, float, str]: + """Return (lat, lon, display_name) for a city.""" + params = urllib.parse.urlencode({"name": city, "count": 1, "format": "json"}) + url = f"https://geocoding-api.open-meteo.com/v1/search?{params}" + with urllib.request.urlopen(url, timeout=10) as resp: + data = json.loads(resp.read()) + results = data.get("results", []) + if not results: + raise ValueError(f"City not found: {city!r}") + r = results[0] + return r["latitude"], r["longitude"], f"{r['name']}, {r.get('country', '')}" + + +def get_weather(lat: float, lon: float) -> dict: + """Fetch current weather from Open-Meteo.""" + params = urllib.parse.urlencode({ + "latitude": lat, "longitude": lon, + "current_weather": "true", + "timezone": "auto", + "forecast_days": 1, + }) + url = f"https://api.open-meteo.com/v1/forecast?{params}" + with urllib.request.urlopen(url, timeout=10) as resp: + return json.loads(resp.read()) + + +def display_weather(city_name: str, data: dict) -> None: + cw = data["current_weather"] + temp_c = cw["temperature"] + temp_f = temp_c * 9 / 5 + 32 + wind = cw["windspeed"] + code = int(cw["weathercode"]) + condition = WMO_CODES.get(code, "Unknown") + is_day = cw.get("is_day", 1) + icon = "☀️" if is_day and code == 0 else "⛈️" if code >= 95 else "🌧️" if 60 <= code < 70 else "🌤️" + + print(f""" + ╔══════════════════════════════════════╗ + {icon} {city_name} + ╠══════════════════════════════════════╣ + Condition : {condition} + Temperature : {temp_c}°C / {temp_f:.1f}°F + Wind Speed : {wind} km/h + ╚══════════════════════════════════════╝""") + + +def main() -> None: + print("=" * 45) + print(" WEATHER CLI (Open-Meteo)") + print("=" * 45) + while True: + city = input("\nCity name (or 'quit'): ").strip() + if city.lower() in ("quit", "q", ""): + break + try: + lat, lon, name = geocode(city) + weather = get_weather(lat, lon) + display_weather(name, weather) + except ValueError as e: + print(f" {e}") + except Exception as e: + print(f" Network error: {e}") + + +if __name__ == "__main__": + main() diff --git a/src/content/day-29/lesson.md b/src/content/day-29/lesson.md new file mode 100644 index 0000000..56cebeb --- /dev/null +++ b/src/content/day-29/lesson.md @@ -0,0 +1,275 @@ +# Day 29 🧪 - Testing with pytest + +--- + +## Overview + +Professional Python developers write tests. Learn pytest, the industry-standard testing framework, and adopt Test-Driven Development habits from day one. + +**What you will learn today:** + +- Why tests matter and what they catch +- Writing `assert` statements +- pytest test discovery conventions +- Fixtures with `@pytest.fixture` +- Parametrize with `@pytest.mark.parametrize` +- Testing exceptions with `pytest.raises` + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `pytest` | A third-party testing framework. Install with `pip install pytest`. Run tests with `pytest` in the terminal. | +| `assert` | Verifies a condition is True. If False, the test fails and pytest shows a clear diff. | +| `fixture` | A function decorated with `@pytest.fixture` that sets up shared test data or state. | +| `parametrize` | Run the same test with multiple inputs. Reduces duplicated test code dramatically. | +| `pytest.raises` | Context manager that asserts a specific exception is raised inside the block. | + +--- + +## Code Examples + +### Your first pytest tests + +```python +# test_calculator.py (file must start with test_) + +def add(a: int, b: int) -> int: + return a + b + +def divide(a: float, b: float) -> float: + if b == 0: + raise ValueError("Cannot divide by zero") + return a / b + +# Test functions must start with test_ +def test_add_positive_numbers(): + assert add(2, 3) == 5 + +def test_add_negative(): + assert add(-1, -1) == -2 + +def test_add_zero(): + assert add(0, 100) == 100 + +# Run: pytest test_calculator.py -v +``` + +### Fixtures and parametrize + +```python +import pytest + +@pytest.fixture +def sample_list(): + """Provide a fresh list for each test.""" + return [1, 2, 3, 4, 5] + +def test_list_length(sample_list): + assert len(sample_list) == 5 + +def test_list_sum(sample_list): + assert sum(sample_list) == 15 + +# Parametrize: run test with multiple inputs +@pytest.mark.parametrize("a, b, expected", [ + (2, 3, 5), + (-1, 1, 0), + (0, 0, 0), + (100, 200, 300), +]) +def test_add_parametrized(a, b, expected): + assert add(a, b) == expected + +# Test that exceptions are raised +def test_divide_by_zero(): + with pytest.raises(ValueError, match="Cannot divide by zero"): + divide(10, 0) +``` + +--- + +## Today's Project: Full Test Suite + +> Write a complete pytest test suite for a `BankAccount` class, covering happy paths, edge cases, and exception handling. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 29 Project: Full Test Suite +================================ +Complete pytest tests for a BankAccount class. + +Run with: pytest solution.py -v +""" +import pytest + +# ── Code Under Test ────────────────────────────────────────────────────── + +class InsufficientFundsError(Exception): + """Raised when a withdrawal exceeds the balance.""" + +class BankAccount: + """A simple bank account.""" + + def __init__(self, owner: str, balance: float = 0.0) -> None: + if balance < 0: + raise ValueError("Initial balance cannot be negative.") + self.owner = owner + self._balance = balance + self._transactions: list[dict] = [] + + @property + def balance(self) -> float: + return self._balance + + def deposit(self, amount: float) -> None: + if amount <= 0: + raise ValueError("Deposit amount must be positive.") + self._balance += amount + self._transactions.append({"type": "deposit", "amount": amount}) + + def withdraw(self, amount: float) -> None: + if amount <= 0: + raise ValueError("Withdrawal amount must be positive.") + if amount > self._balance: + raise InsufficientFundsError( + f"Cannot withdraw {amount}: balance is {self._balance}" + ) + self._balance -= amount + self._transactions.append({"type": "withdraw", "amount": amount}) + + def transfer(self, amount: float, target: "BankAccount") -> None: + self.withdraw(amount) + target.deposit(amount) + + @property + def transaction_count(self) -> int: + return len(self._transactions) + +# ── Fixtures ───────────────────────────────────────────────────────────── + +@pytest.fixture +def empty_account(): + """A brand-new account with zero balance.""" + return BankAccount("Alice") + +@pytest.fixture +def funded_account(): + """An account with $1000 balance.""" + return BankAccount("Bob", balance=1000.0) + +@pytest.fixture +def two_accounts(): + """Two accounts for transfer tests.""" + return BankAccount("Alice", 500.0), BankAccount("Bob", 200.0) + +# ── Tests: Initialization ───────────────────────────────────────────────── + +class TestInit: + def test_default_balance_is_zero(self, empty_account): + assert empty_account.balance == 0.0 + + def test_initial_balance_set(self, funded_account): + assert funded_account.balance == 1000.0 + + def test_owner_stored(self, funded_account): + assert funded_account.owner == "Bob" + + def test_negative_initial_balance_raises(self): + with pytest.raises(ValueError, match="cannot be negative"): + BankAccount("X", balance=-100.0) + +# ── Tests: Deposit ──────────────────────────────────────────────────────── + +class TestDeposit: + def test_deposit_increases_balance(self, empty_account): + empty_account.deposit(100.0) + assert empty_account.balance == 100.0 + + def test_multiple_deposits(self, empty_account): + empty_account.deposit(50.0) + empty_account.deposit(75.0) + assert empty_account.balance == 125.0 + + @pytest.mark.parametrize("amount", [0.01, 1.0, 999.99, 1_000_000.0]) + def test_valid_deposit_amounts(self, empty_account, amount): + empty_account.deposit(amount) + assert empty_account.balance == pytest.approx(amount) + + def test_zero_deposit_raises(self, empty_account): + with pytest.raises(ValueError, match="must be positive"): + empty_account.deposit(0) + + def test_negative_deposit_raises(self, empty_account): + with pytest.raises(ValueError): + empty_account.deposit(-50.0) + + def test_deposit_records_transaction(self, empty_account): + empty_account.deposit(100.0) + assert empty_account.transaction_count == 1 + +# ── Tests: Withdrawal ───────────────────────────────────────────────────── + +class TestWithdraw: + def test_withdraw_decreases_balance(self, funded_account): + funded_account.withdraw(200.0) + assert funded_account.balance == 800.0 + + def test_withdraw_entire_balance(self, funded_account): + funded_account.withdraw(1000.0) + assert funded_account.balance == 0.0 + + def test_overdraft_raises(self, funded_account): + with pytest.raises(InsufficientFundsError): + funded_account.withdraw(1001.0) + + def test_zero_withdrawal_raises(self, funded_account): + with pytest.raises(ValueError): + funded_account.withdraw(0) + + def test_negative_withdrawal_raises(self, funded_account): + with pytest.raises(ValueError): + funded_account.withdraw(-100.0) + + def test_balance_unchanged_after_failed_withdraw(self, funded_account): + try: + funded_account.withdraw(9999.0) + except InsufficientFundsError: + pass + assert funded_account.balance == 1000.0 + +# ── Tests: Transfer ─────────────────────────────────────────────────────── + +class TestTransfer: + def test_transfer_moves_funds(self, two_accounts): + alice, bob = two_accounts + alice.transfer(100.0, bob) + assert alice.balance == 400.0 + assert bob.balance == 300.0 + + def test_transfer_insufficient_funds_raises(self, two_accounts): + alice, bob = two_accounts + with pytest.raises(InsufficientFundsError): + alice.transfer(9999.0, bob) + + def test_failed_transfer_leaves_balances_unchanged(self, two_accounts): + alice, bob = two_accounts + try: + alice.transfer(9999.0, bob) + except InsufficientFundsError: + pass + assert alice.balance == 500.0 + assert bob.balance == 200.0 +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-29/solution.py b/src/content/day-29/solution.py new file mode 100644 index 0000000..35ff33c --- /dev/null +++ b/src/content/day-29/solution.py @@ -0,0 +1,175 @@ +""" +Day 29 Project: Full Test Suite +================================ +Complete pytest tests for a BankAccount class. + +Run with: pytest solution.py -v +""" +import pytest + + +# ── Code Under Test ────────────────────────────────────────────────────── + +class InsufficientFundsError(Exception): + """Raised when a withdrawal exceeds the balance.""" + + +class BankAccount: + """A simple bank account.""" + + def __init__(self, owner: str, balance: float = 0.0) -> None: + if balance < 0: + raise ValueError("Initial balance cannot be negative.") + self.owner = owner + self._balance = balance + self._transactions: list[dict] = [] + + @property + def balance(self) -> float: + return self._balance + + def deposit(self, amount: float) -> None: + if amount <= 0: + raise ValueError("Deposit amount must be positive.") + self._balance += amount + self._transactions.append({"type": "deposit", "amount": amount}) + + def withdraw(self, amount: float) -> None: + if amount <= 0: + raise ValueError("Withdrawal amount must be positive.") + if amount > self._balance: + raise InsufficientFundsError( + f"Cannot withdraw {amount}: balance is {self._balance}" + ) + self._balance -= amount + self._transactions.append({"type": "withdraw", "amount": amount}) + + def transfer(self, amount: float, target: "BankAccount") -> None: + self.withdraw(amount) + target.deposit(amount) + + @property + def transaction_count(self) -> int: + return len(self._transactions) + + +# ── Fixtures ───────────────────────────────────────────────────────────── + +@pytest.fixture +def empty_account(): + """A brand-new account with zero balance.""" + return BankAccount("Alice") + + +@pytest.fixture +def funded_account(): + """An account with $1000 balance.""" + return BankAccount("Bob", balance=1000.0) + + +@pytest.fixture +def two_accounts(): + """Two accounts for transfer tests.""" + return BankAccount("Alice", 500.0), BankAccount("Bob", 200.0) + + +# ── Tests: Initialization ───────────────────────────────────────────────── + +class TestInit: + def test_default_balance_is_zero(self, empty_account): + assert empty_account.balance == 0.0 + + def test_initial_balance_set(self, funded_account): + assert funded_account.balance == 1000.0 + + def test_owner_stored(self, funded_account): + assert funded_account.owner == "Bob" + + def test_negative_initial_balance_raises(self): + with pytest.raises(ValueError, match="cannot be negative"): + BankAccount("X", balance=-100.0) + + +# ── Tests: Deposit ──────────────────────────────────────────────────────── + +class TestDeposit: + def test_deposit_increases_balance(self, empty_account): + empty_account.deposit(100.0) + assert empty_account.balance == 100.0 + + def test_multiple_deposits(self, empty_account): + empty_account.deposit(50.0) + empty_account.deposit(75.0) + assert empty_account.balance == 125.0 + + @pytest.mark.parametrize("amount", [0.01, 1.0, 999.99, 1_000_000.0]) + def test_valid_deposit_amounts(self, empty_account, amount): + empty_account.deposit(amount) + assert empty_account.balance == pytest.approx(amount) + + def test_zero_deposit_raises(self, empty_account): + with pytest.raises(ValueError, match="must be positive"): + empty_account.deposit(0) + + def test_negative_deposit_raises(self, empty_account): + with pytest.raises(ValueError): + empty_account.deposit(-50.0) + + def test_deposit_records_transaction(self, empty_account): + empty_account.deposit(100.0) + assert empty_account.transaction_count == 1 + + +# ── Tests: Withdrawal ───────────────────────────────────────────────────── + +class TestWithdraw: + def test_withdraw_decreases_balance(self, funded_account): + funded_account.withdraw(200.0) + assert funded_account.balance == 800.0 + + def test_withdraw_entire_balance(self, funded_account): + funded_account.withdraw(1000.0) + assert funded_account.balance == 0.0 + + def test_overdraft_raises(self, funded_account): + with pytest.raises(InsufficientFundsError): + funded_account.withdraw(1001.0) + + def test_zero_withdrawal_raises(self, funded_account): + with pytest.raises(ValueError): + funded_account.withdraw(0) + + def test_negative_withdrawal_raises(self, funded_account): + with pytest.raises(ValueError): + funded_account.withdraw(-100.0) + + def test_balance_unchanged_after_failed_withdraw(self, funded_account): + try: + funded_account.withdraw(9999.0) + except InsufficientFundsError: + pass + assert funded_account.balance == 1000.0 + + +# ── Tests: Transfer ─────────────────────────────────────────────────────── + +class TestTransfer: + def test_transfer_moves_funds(self, two_accounts): + alice, bob = two_accounts + alice.transfer(100.0, bob) + assert alice.balance == 400.0 + assert bob.balance == 300.0 + + def test_transfer_insufficient_funds_raises(self, two_accounts): + alice, bob = two_accounts + with pytest.raises(InsufficientFundsError): + alice.transfer(9999.0, bob) + + def test_failed_transfer_leaves_balances_unchanged(self, two_accounts): + alice, bob = two_accounts + try: + alice.transfer(9999.0, bob) + except InsufficientFundsError: + pass + assert alice.balance == 500.0 + assert bob.balance == 200.0 diff --git a/src/content/day-30/lesson.md b/src/content/day-30/lesson.md new file mode 100644 index 0000000..3074d2a --- /dev/null +++ b/src/content/day-30/lesson.md @@ -0,0 +1,373 @@ +# Day 30 🏆 - Final Project: CLI Todo App + +--- + +## Overview + +Congratulations on reaching Day 30! Today you build a complete, production-quality command-line Todo application that brings together every concept you have learned. + +**What you will learn today:** + +- Combining OOP, file I/O, JSON, error handling, and type hints +- CLI argument parsing with `argparse` +- Persistent storage with JSON +- Professional project structure +- Writing a complete `README` for your project +- Next steps: what to learn after this course + +--- + +## Key Concepts + +| Concept | Description | +|---|---| +| `argparse` | Standard library module for building professional command-line interfaces with help text, flags, and subcommands. | +| `dataclass` | Decorator that auto-generates `__init__`, `__repr__`, and `__eq__` from class annotations. Perfect for data containers. | +| `json persistence` | Serialize your application state to a JSON file so data survives between program runs. | +| `sys.exit()` | Exit the program with a status code. `sys.exit(0)` = success, `sys.exit(1)` = error. | + +--- + +## Code Examples + +### argparse basics + +```python +import argparse + +parser = argparse.ArgumentParser( + prog="todo", + description="A command-line todo manager", +) +subparsers = parser.add_subparsers(dest="command") + +# todo add "Buy groceries" --priority high +add_parser = subparsers.add_parser("add", help="Add a new task") +add_parser.add_argument("title", help="Task title") +add_parser.add_argument("--priority", choices=["low", "medium", "high"], + default="medium") + +# todo list +subparsers.add_parser("list", help="List all tasks") + +# todo done 3 +done_parser = subparsers.add_parser("done", help="Mark task as done") +done_parser.add_argument("id", type=int, help="Task ID") + +args = parser.parse_args() +print(args) +``` + +### dataclasses + +```python +from dataclasses import dataclass, field +from datetime import datetime + +@dataclass +class Task: + title: str + priority: str = "medium" + done: bool = False + id: int = 0 + created_at: str = field(default_factory=lambda: datetime.now().isoformat()) + + def __str__(self) -> str: + status = "DONE" if self.done else "TODO" + return f"[{status}] #{self.id} {self.title} ({self.priority})" + +t = Task("Buy groceries", priority="high") +print(t) # [TODO] #0 Buy groceries (high) +``` + +--- + +## Today's Project: CLI Todo Manager + +> A fully featured command-line Todo application with add, list, done, delete, filter, and export commands, persistent JSON storage, and a clean `--help` interface. + +**View the full project code in the "Project Solution" panel below.** + +```python +""" +Day 30 Final Project: CLI Todo Manager +======================================== +A complete command-line task manager combining: + OOP | dataclasses | JSON persistence | argparse | type hints + +Usage: + python solution.py add "Buy groceries" --priority high + python solution.py list + python solution.py list --filter pending + python solution.py done 1 + python solution.py delete 2 + python solution.py export tasks_backup.json + python solution.py stats +""" +import argparse +import json +import sys +from dataclasses import asdict, dataclass, field +from datetime import datetime +from pathlib import Path + +# ── Data Model ─────────────────────────────────────────────────────────── + +@dataclass +class Task: + """A single todo task.""" + title: str + priority: str = "medium" + done: bool = False + task_id: int = 0 + created_at: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M")) + completed_at: str = "" + + @property + def status_icon(self) -> str: + return "✓" if self.done else "○" + + @property + def priority_icon(self) -> str: + return {"high": "!!!", "medium": "! ", "low": " "}.get(self.priority, " ") + + def __str__(self) -> str: + done_str = f" [done {self.completed_at}]" if self.done else "" + return (f" [{self.status_icon}] #{self.task_id:<3} " + f"{self.priority_icon} {self.title}{done_str}") + +# ── Storage ─────────────────────────────────────────────────────────────── + +class TaskStore: + """Persist tasks to a JSON file.""" + + def __init__(self, path: Path) -> None: + self.path = path + self._tasks: list[Task] = [] + self._next_id: int = 1 + self._load() + + def _load(self) -> None: + if self.path.exists(): + try: + raw = json.loads(self.path.read_text(encoding="utf-8")) + self._tasks = [Task(**t) for t in raw.get("tasks", [])] + self._next_id = raw.get("next_id", 1) + except (json.JSONDecodeError, TypeError): + self._tasks = [] + self._next_id = 1 + + def _save(self) -> None: + data = { + "next_id": self._next_id, + "tasks": [asdict(t) for t in self._tasks], + } + self.path.write_text(json.dumps(data, indent=2), encoding="utf-8") + + def add(self, title: str, priority: str) -> Task: + task = Task(title=title, priority=priority, task_id=self._next_id) + self._next_id += 1 + self._tasks.append(task) + self._save() + return task + + def get(self, task_id: int) -> Task | None: + return next((t for t in self._tasks if t.task_id == task_id), None) + + def all_tasks(self) -> list[Task]: + return list(self._tasks) + + def pending(self) -> list[Task]: + return [t for t in self._tasks if not t.done] + + def completed(self) -> list[Task]: + return [t for t in self._tasks if t.done] + + def mark_done(self, task_id: int) -> Task | None: + task = self.get(task_id) + if task and not task.done: + task.done = True + task.completed_at = datetime.now().strftime("%Y-%m-%d %H:%M") + self._save() + return task + + def delete(self, task_id: int) -> bool: + original = len(self._tasks) + self._tasks = [t for t in self._tasks if t.task_id != task_id] + if len(self._tasks) < original: + self._save() + return True + return False + + def export(self, dest: Path) -> None: + dest.write_text(json.dumps([asdict(t) for t in self._tasks], indent=2)) + +# ── CLI Commands ────────────────────────────────────────────────────────── + +def cmd_add(store: TaskStore, args: argparse.Namespace) -> int: + task = store.add(args.title, args.priority) + print(f" Added task #{task.task_id}: {task.title!r} [{task.priority}]") + return 0 + +def cmd_list(store: TaskStore, args: argparse.Namespace) -> int: + filter_mode = getattr(args, "filter", "all") + tasks = { + "all": store.all_tasks(), + "pending": store.pending(), + "done": store.completed(), + }.get(filter_mode, store.all_tasks()) + + if not tasks: + print(" No tasks found.") + return 0 + + print(f"\n {'='*50}") + print(f" TODO LIST ({len(tasks)} task(s) | filter: {filter_mode})") + print(f" {'='*50}") + + by_priority = {"high": [], "medium": [], "low": []} + for t in tasks: + by_priority.setdefault(t.priority, []).append(t) + + for prio in ("high", "medium", "low"): + group = by_priority[prio] + if group: + print(f"\n -- {prio.upper()} --") + for t in group: + print(t) + + print() + return 0 + +def cmd_done(store: TaskStore, args: argparse.Namespace) -> int: + task = store.mark_done(args.id) + if task: + print(f" Marked done: #{task.task_id} {task.title!r}") + else: + print(f" Task #{args.id} not found or already done.") + return 1 + return 0 + +def cmd_delete(store: TaskStore, args: argparse.Namespace) -> int: + if store.delete(args.id): + print(f" Deleted task #{args.id}.") + else: + print(f" Task #{args.id} not found.") + return 1 + return 0 + +def cmd_stats(store: TaskStore, _: argparse.Namespace) -> int: + all_t = store.all_tasks() + done = store.completed() + pend = store.pending() + pct = len(done) / len(all_t) * 100 if all_t else 0 + bar = "#" * int(pct / 5) + "." * (20 - int(pct / 5)) + + print(f""" + {'='*45} + TODO STATS + {'='*45} + Total tasks : {len(all_t)} + Completed : {len(done)} + Pending : {len(pend)} + Progress : [{bar}] {pct:.0f}% + {'='*45}""") + if pend: + high = [t for t in pend if t.priority == "high"] + if high: + print(f" High-priority pending ({len(high)}):") + for t in high: + print(f" #{t.task_id} {t.title}") + return 0 + +def cmd_export(store: TaskStore, args: argparse.Namespace) -> int: + dest = Path(args.file) + store.export(dest) + print(f" Exported {len(store.all_tasks())} task(s) to {dest}") + return 0 + +# ── Argument Parser ─────────────────────────────────────────────────────── + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="todo", + description="A professional CLI todo manager, Day 30 Final Project", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +examples: + python solution.py add "Buy groceries" --priority high + python solution.py list --filter pending + python solution.py done 1 + python solution.py delete 2 + python solution.py stats + python solution.py export backup.json + """, + ) + parser.add_argument( + "--file", default="todos.json", + help="Path to the JSON storage file (default: todos.json)" + ) + sub = parser.add_subparsers(dest="command", metavar="COMMAND") + sub.required = True + + # add + p_add = sub.add_parser("add", help="Add a new task") + p_add.add_argument("title", help="Task title") + p_add.add_argument("--priority", choices=["low", "medium", "high"], + default="medium", help="Task priority (default: medium)") + + # list + p_list = sub.add_parser("list", help="List tasks") + p_list.add_argument("--filter", choices=["all", "pending", "done"], + default="all", help="Filter tasks (default: all)") + + # done + p_done = sub.add_parser("done", help="Mark a task as done") + p_done.add_argument("id", type=int, help="Task ID") + + # delete + p_del = sub.add_parser("delete", help="Delete a task") + p_del.add_argument("id", type=int, help="Task ID") + + # stats + sub.add_parser("stats", help="Show completion statistics") + + # export + p_exp = sub.add_parser("export", help="Export tasks to a JSON file") + p_exp.add_argument("file", help="Destination JSON file path") + + return parser + +# ── Main ────────────────────────────────────────────────────────────────── + +COMMANDS = { + "add": cmd_add, + "list": cmd_list, + "done": cmd_done, + "delete": cmd_delete, + "stats": cmd_stats, + "export": cmd_export, +} + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + store = TaskStore(Path(args.file)) + handler = COMMANDS.get(args.command) + if handler: + sys.exit(handler(store, args)) + else: + parser.print_help() + sys.exit(1) + +if __name__ == "__main__": + main() +``` + +--- + +## Knowledge Check + +Before moving on, make sure you can explain each concept without looking at the lesson. + +--- diff --git a/src/content/day-30/solution.py b/src/content/day-30/solution.py new file mode 100644 index 0000000..ec495f7 --- /dev/null +++ b/src/content/day-30/solution.py @@ -0,0 +1,285 @@ +""" +Day 30 Final Project: CLI Todo Manager +======================================== +A complete command-line task manager combining: + OOP | dataclasses | JSON persistence | argparse | type hints + +Usage: + python solution.py add "Buy groceries" --priority high + python solution.py list + python solution.py list --filter pending + python solution.py done 1 + python solution.py delete 2 + python solution.py export tasks_backup.json + python solution.py stats +""" +import argparse +import json +import sys +from dataclasses import asdict, dataclass, field +from datetime import datetime +from pathlib import Path + + +# ── Data Model ─────────────────────────────────────────────────────────── + +@dataclass +class Task: + """A single todo task.""" + title: str + priority: str = "medium" + done: bool = False + task_id: int = 0 + created_at: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M")) + completed_at: str = "" + + @property + def status_icon(self) -> str: + return "✓" if self.done else "○" + + @property + def priority_icon(self) -> str: + return {"high": "!!!", "medium": "! ", "low": " "}.get(self.priority, " ") + + def __str__(self) -> str: + done_str = f" [done {self.completed_at}]" if self.done else "" + return (f" [{self.status_icon}] #{self.task_id:<3} " + f"{self.priority_icon} {self.title}{done_str}") + + +# ── Storage ─────────────────────────────────────────────────────────────── + +class TaskStore: + """Persist tasks to a JSON file.""" + + def __init__(self, path: Path) -> None: + self.path = path + self._tasks: list[Task] = [] + self._next_id: int = 1 + self._load() + + def _load(self) -> None: + if self.path.exists(): + try: + raw = json.loads(self.path.read_text(encoding="utf-8")) + self._tasks = [Task(**t) for t in raw.get("tasks", [])] + self._next_id = raw.get("next_id", 1) + except (json.JSONDecodeError, TypeError): + self._tasks = [] + self._next_id = 1 + + def _save(self) -> None: + data = { + "next_id": self._next_id, + "tasks": [asdict(t) for t in self._tasks], + } + self.path.write_text(json.dumps(data, indent=2), encoding="utf-8") + + def add(self, title: str, priority: str) -> Task: + task = Task(title=title, priority=priority, task_id=self._next_id) + self._next_id += 1 + self._tasks.append(task) + self._save() + return task + + def get(self, task_id: int) -> Task | None: + return next((t for t in self._tasks if t.task_id == task_id), None) + + def all_tasks(self) -> list[Task]: + return list(self._tasks) + + def pending(self) -> list[Task]: + return [t for t in self._tasks if not t.done] + + def completed(self) -> list[Task]: + return [t for t in self._tasks if t.done] + + def mark_done(self, task_id: int) -> Task | None: + task = self.get(task_id) + if task and not task.done: + task.done = True + task.completed_at = datetime.now().strftime("%Y-%m-%d %H:%M") + self._save() + return task + + def delete(self, task_id: int) -> bool: + original = len(self._tasks) + self._tasks = [t for t in self._tasks if t.task_id != task_id] + if len(self._tasks) < original: + self._save() + return True + return False + + def export(self, dest: Path) -> None: + dest.write_text(json.dumps([asdict(t) for t in self._tasks], indent=2)) + + +# ── CLI Commands ────────────────────────────────────────────────────────── + +def cmd_add(store: TaskStore, args: argparse.Namespace) -> int: + task = store.add(args.title, args.priority) + print(f" Added task #{task.task_id}: {task.title!r} [{task.priority}]") + return 0 + + +def cmd_list(store: TaskStore, args: argparse.Namespace) -> int: + filter_mode = getattr(args, "filter", "all") + tasks = { + "all": store.all_tasks(), + "pending": store.pending(), + "done": store.completed(), + }.get(filter_mode, store.all_tasks()) + + if not tasks: + print(" No tasks found.") + return 0 + + print(f"\n {'='*50}") + print(f" TODO LIST ({len(tasks)} task(s) | filter: {filter_mode})") + print(f" {'='*50}") + + by_priority = {"high": [], "medium": [], "low": []} + for t in tasks: + by_priority.setdefault(t.priority, []).append(t) + + for prio in ("high", "medium", "low"): + group = by_priority[prio] + if group: + print(f"\n -- {prio.upper()} --") + for t in group: + print(t) + + print() + return 0 + + +def cmd_done(store: TaskStore, args: argparse.Namespace) -> int: + task = store.mark_done(args.id) + if task: + print(f" Marked done: #{task.task_id} {task.title!r}") + else: + print(f" Task #{args.id} not found or already done.") + return 1 + return 0 + + +def cmd_delete(store: TaskStore, args: argparse.Namespace) -> int: + if store.delete(args.id): + print(f" Deleted task #{args.id}.") + else: + print(f" Task #{args.id} not found.") + return 1 + return 0 + + +def cmd_stats(store: TaskStore, _: argparse.Namespace) -> int: + all_t = store.all_tasks() + done = store.completed() + pend = store.pending() + pct = len(done) / len(all_t) * 100 if all_t else 0 + bar = "#" * int(pct / 5) + "." * (20 - int(pct / 5)) + + print(f""" + {'='*45} + TODO STATS + {'='*45} + Total tasks : {len(all_t)} + Completed : {len(done)} + Pending : {len(pend)} + Progress : [{bar}] {pct:.0f}% + {'='*45}""") + if pend: + high = [t for t in pend if t.priority == "high"] + if high: + print(f" High-priority pending ({len(high)}):") + for t in high: + print(f" #{t.task_id} {t.title}") + return 0 + + +def cmd_export(store: TaskStore, args: argparse.Namespace) -> int: + dest = Path(args.file) + store.export(dest) + print(f" Exported {len(store.all_tasks())} task(s) to {dest}") + return 0 + + +# ── Argument Parser ─────────────────────────────────────────────────────── + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="todo", + description="A professional CLI todo manager, Day 30 Final Project", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +examples: + python solution.py add "Buy groceries" --priority high + python solution.py list --filter pending + python solution.py done 1 + python solution.py delete 2 + python solution.py stats + python solution.py export backup.json + """, + ) + parser.add_argument( + "--file", default="todos.json", + help="Path to the JSON storage file (default: todos.json)" + ) + sub = parser.add_subparsers(dest="command", metavar="COMMAND") + sub.required = True + + # add + p_add = sub.add_parser("add", help="Add a new task") + p_add.add_argument("title", help="Task title") + p_add.add_argument("--priority", choices=["low", "medium", "high"], + default="medium", help="Task priority (default: medium)") + + # list + p_list = sub.add_parser("list", help="List tasks") + p_list.add_argument("--filter", choices=["all", "pending", "done"], + default="all", help="Filter tasks (default: all)") + + # done + p_done = sub.add_parser("done", help="Mark a task as done") + p_done.add_argument("id", type=int, help="Task ID") + + # delete + p_del = sub.add_parser("delete", help="Delete a task") + p_del.add_argument("id", type=int, help="Task ID") + + # stats + sub.add_parser("stats", help="Show completion statistics") + + # export + p_exp = sub.add_parser("export", help="Export tasks to a JSON file") + p_exp.add_argument("file", help="Destination JSON file path") + + return parser + + +# ── Main ────────────────────────────────────────────────────────────────── + +COMMANDS = { + "add": cmd_add, + "list": cmd_list, + "done": cmd_done, + "delete": cmd_delete, + "stats": cmd_stats, + "export": cmd_export, +} + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + store = TaskStore(Path(args.file)) + handler = COMMANDS.get(args.command) + if handler: + sys.exit(handler(store, args)) + else: + parser.print_help() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..73a48eb --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,102 @@ +import React, { createContext, useContext, useEffect, useMemo, useState } from 'react' + +/** + * This is a SHOWCASE authentication system only. + * + * Nothing in this app actually requires an account: + * - All 30 lessons are open and navigable without logging in. + * - The certificate can be generated without logging in. + * - No backend exists; "accounts" are stored in the browser's localStorage + * purely so the login / signup screens have something to demonstrate. + * + * Do not use this pattern for a real product. It exists so the UI can + * demonstrate what an authenticated experience could look like. + */ + +export interface ShowcaseUser { + name: string + email: string +} + +interface AuthContextValue { + user: ShowcaseUser | null + login: (email: string, _password: string) => { ok: boolean; error?: string } + signup: (name: string, email: string, _password: string) => { ok: boolean; error?: string } + logout: () => void +} + +const STORAGE_KEY = 'p30_showcase_user' +const ACCOUNTS_KEY = 'p30_showcase_accounts' + +const AuthContext = createContext(undefined) + +type StoredAccount = { name: string; email: string; password: string } + +function readAccounts(): StoredAccount[] { + try { + const raw = localStorage.getItem(ACCOUNTS_KEY) + return raw ? (JSON.parse(raw) as StoredAccount[]) : [] + } catch { + return [] + } +} + +function writeAccounts(accounts: StoredAccount[]) { + localStorage.setItem(ACCOUNTS_KEY, JSON.stringify(accounts)) +} + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null) + + useEffect(() => { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (raw) setUser(JSON.parse(raw) as ShowcaseUser) + } catch { + /* ignore corrupted storage */ + } + }, []) + + const login: AuthContextValue['login'] = (email, _password) => { + const accounts = readAccounts() + const match = accounts.find((a) => a.email.toLowerCase() === email.toLowerCase()) + if (!match) { + return { ok: false, error: 'No showcase account found for that email. Try signing up first.' } + } + const nextUser = { name: match.name, email: match.email } + setUser(nextUser) + localStorage.setItem(STORAGE_KEY, JSON.stringify(nextUser)) + return { ok: true } + } + + const signup: AuthContextValue['signup'] = (name, email, password) => { + if (!name.trim() || !email.trim() || !password.trim()) { + return { ok: false, error: 'Please fill in every field.' } + } + const accounts = readAccounts() + if (accounts.some((a) => a.email.toLowerCase() === email.toLowerCase())) { + return { ok: false, error: 'An account with that email already exists in this browser.' } + } + accounts.push({ name, email, password }) + writeAccounts(accounts) + const nextUser = { name, email } + setUser(nextUser) + localStorage.setItem(STORAGE_KEY, JSON.stringify(nextUser)) + return { ok: true } + } + + const logout = () => { + setUser(null) + localStorage.removeItem(STORAGE_KEY) + } + + const value = useMemo(() => ({ user, login, signup, logout }), [user]) + + return {children} +} + +export function useAuth() { + const ctx = useContext(AuthContext) + if (!ctx) throw new Error('useAuth must be used within an AuthProvider') + return ctx +} diff --git a/src/context/ProgressContext.tsx b/src/context/ProgressContext.tsx new file mode 100644 index 0000000..5b366e9 --- /dev/null +++ b/src/context/ProgressContext.tsx @@ -0,0 +1,66 @@ +import React, { createContext, useContext, useEffect, useMemo, useState } from 'react' +import { TOTAL_DAYS } from '../data/curriculum' + +/** + * Tracks which days the learner has marked complete. This is entirely + * optional bookkeeping: nothing in the app checks this state before + * allowing navigation to the next or previous day. Learners are always + * free to move forward or backward through the curriculum, and the + * certificate is available regardless of how many days are marked done. + */ + +interface ProgressContextValue { + completedDays: number[] + isComplete: (day: number) => boolean + toggleComplete: (day: number) => void + completedCount: number + percentComplete: number + resetProgress: () => void +} + +const STORAGE_KEY = 'p30_progress' + +const ProgressContext = createContext(undefined) + +export function ProgressProvider({ children }: { children: React.ReactNode }) { + const [completedDays, setCompletedDays] = useState([]) + + useEffect(() => { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (raw) setCompletedDays(JSON.parse(raw) as number[]) + } catch { + /* ignore corrupted storage */ + } + }, []) + + useEffect(() => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(completedDays)) + }, [completedDays]) + + const isComplete = (day: number) => completedDays.includes(day) + + const toggleComplete = (day: number) => { + setCompletedDays((prev) => + prev.includes(day) ? prev.filter((d) => d !== day) : [...prev, day].sort((a, b) => a - b), + ) + } + + const resetProgress = () => setCompletedDays([]) + + const completedCount = completedDays.length + const percentComplete = Math.round((completedCount / TOTAL_DAYS) * 100) + + const value = useMemo( + () => ({ completedDays, isComplete, toggleComplete, completedCount, percentComplete, resetProgress }), + [completedDays], + ) + + return {children} +} + +export function useProgress() { + const ctx = useContext(ProgressContext) + if (!ctx) throw new Error('useProgress must be used within a ProgressProvider') + return ctx +} diff --git a/src/data/content.ts b/src/data/content.ts new file mode 100644 index 0000000..2a6a15e --- /dev/null +++ b/src/data/content.ts @@ -0,0 +1,29 @@ +// Loads the raw lesson markdown and solution source code for every day +// using Vite's built-in `?raw` import glob. This keeps each day's content +// as a plain text asset bundled at build time, no runtime fetch required +// which makes the app fully static and GitHub Pages friendly. + +const lessonModules = import.meta.glob('../content/day-*/lesson.md', { + query: '?raw', + import: 'default', + eager: true, +}) as Record + +const solutionModules = import.meta.glob('../content/day-*/solution.py', { + query: '?raw', + import: 'default', + eager: true, +}) as Record + +function extract(modules: Record, slug: string): string { + const entry = Object.entries(modules).find(([path]) => path.includes(`/${slug}/`)) + return entry ? entry[1] : '' +} + +export function getLessonMarkdown(slug: string): string { + return extract(lessonModules, slug) +} + +export function getSolutionCode(slug: string): string { + return extract(solutionModules, slug) +} diff --git a/src/data/curriculum.ts b/src/data/curriculum.ts new file mode 100644 index 0000000..08db979 --- /dev/null +++ b/src/data/curriculum.ts @@ -0,0 +1,58 @@ +export interface DayMeta { + id: number + slug: string + emoji: string + title: string + topic: string +} + +// Static metadata for the 30 day curriculum. Kept separate from the raw +// markdown/python content so the navigation, sidebar and home page can +// render instantly without needing to parse every lesson file. +export const CURRICULUM: DayMeta[] = [ + { id: 1, slug: 'day-01', emoji: '🐍', title: 'Setup & Hello World', topic: 'Fundamentals' }, + { id: 2, slug: 'day-02', emoji: '📦', title: 'Variables & Data Types', topic: 'Fundamentals' }, + { id: 3, slug: 'day-03', emoji: '✍️', title: 'Strings & String Methods', topic: 'Fundamentals' }, + { id: 4, slug: 'day-04', emoji: '🔢', title: 'Numbers & Math Operations', topic: 'Fundamentals' }, + { id: 5, slug: 'day-05', emoji: '⌨️', title: 'User Input', topic: 'Fundamentals' }, + { id: 6, slug: 'day-06', emoji: '📋', title: 'Lists', topic: 'Data Structures' }, + { id: 7, slug: 'day-07', emoji: '🎯', title: 'Tuples, Sets & Booleans', topic: 'Data Structures' }, + { id: 8, slug: 'day-08', emoji: '📖', title: 'Dictionaries', topic: 'Data Structures' }, + { id: 9, slug: 'day-09', emoji: '🔀', title: 'Conditionals', topic: 'Control Flow' }, + { id: 10, slug: 'day-10', emoji: '🔄', title: 'for Loops', topic: 'Control Flow' }, + { id: 11, slug: 'day-11', emoji: '🔁', title: 'while Loops', topic: 'Control Flow' }, + { id: 12, slug: 'day-12', emoji: '⚙️', title: 'Functions Basics', topic: 'Functions' }, + { id: 13, slug: 'day-13', emoji: '🎛️', title: 'Function Arguments & Type Hints', topic: 'Functions' }, + { id: 14, slug: 'day-14', emoji: '🔭', title: 'Scope & Closures', topic: 'Functions' }, + { id: 15, slug: 'day-15', emoji: '🛡️', title: 'Error Handling', topic: 'Robust Code' }, + { id: 16, slug: 'day-16', emoji: '📁', title: 'File I/O', topic: 'Robust Code' }, + { id: 17, slug: 'day-17', emoji: '⚡', title: 'List Comprehensions', topic: 'Pythonic Code' }, + { id: 18, slug: 'day-18', emoji: '🗃️', title: 'Lambda, Map, Filter & Sorted', topic: 'Pythonic Code' }, + { id: 19, slug: 'day-19', emoji: '📦', title: 'Modules & Packages', topic: 'Pythonic Code' }, + { id: 20, slug: 'day-20', emoji: '🏗️', title: 'OOP: Classes & Objects', topic: 'Object Oriented Programming' }, + { id: 21, slug: 'day-21', emoji: '🧬', title: 'OOP: Inheritance & Polymorphism', topic: 'Object Oriented Programming' }, + { id: 22, slug: 'day-22', emoji: '🧙', title: 'Dunder Methods', topic: 'Object Oriented Programming' }, + { id: 23, slug: 'day-23', emoji: '🎀', title: 'Decorators', topic: 'Advanced Python' }, + { id: 24, slug: 'day-24', emoji: '♾️', title: 'Generators & Iterators', topic: 'Advanced Python' }, + { id: 25, slug: 'day-25', emoji: '🔒', title: 'Context Managers', topic: 'Advanced Python' }, + { id: 26, slug: 'day-26', emoji: '🔍', title: 'Regular Expressions', topic: 'Real World Data' }, + { id: 27, slug: 'day-27', emoji: '📊', title: 'JSON & CSV Data', topic: 'Real World Data' }, + { id: 28, slug: 'day-28', emoji: '🌐', title: 'Working with APIs', topic: 'Real World Data' }, + { id: 29, slug: 'day-29', emoji: '🧪', title: 'Testing with pytest', topic: 'Professional Practices' }, + { id: 30, slug: 'day-30', emoji: '🏆', title: 'Final Project: CLI Todo App', topic: 'Professional Practices' }, +] + +export const TOTAL_DAYS = CURRICULUM.length + +export function getDayMeta(id: number): DayMeta | undefined { + return CURRICULUM.find((d) => d.id === id) +} + +export function getAdjacentDays(id: number) { + return { + prev: CURRICULUM.find((d) => d.id === id - 1) ?? null, + next: CURRICULUM.find((d) => d.id === id + 1) ?? null, + } +} + +export const TOPICS = Array.from(new Set(CURRICULUM.map((d) => d.topic))) diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..df3946b --- /dev/null +++ b/src/index.css @@ -0,0 +1,169 @@ +:root { + --python-blue: #4b8bbe; + --python-yellow: #ffd43b; + --bg: #0f1115; + --surface: #161923; + --surface-hover: #1e2230; + --border: #262c3b; + --text: #eef1f6; + --text-muted: #a7adbb; + --text-faint: #6b7280; + --accent: #4b8bbe; + --accent-strong: #ffd43b; + --success: #4caf7d; + --radius: 12px; + --shadow: 0 8px 24px rgba(0, 0, 0, 0.25); + color-scheme: dark; +} + +* { + box-sizing: border-box; +} + +html, +body { + padding: 0; + margin: 0; +} + +body { + background: var(--bg); + color: var(--text); + font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +a { + color: inherit; +} + +h1, +h2, +h3, +h4 { + line-height: 1.25; + font-weight: 700; +} + +code, +pre { + font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +button { + font-family: inherit; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6rem 1.1rem; + font-weight: 600; + font-size: 0.9rem; + cursor: pointer; + text-decoration: none; + transition: transform 0.1s ease, opacity 0.15s ease, background 0.15s ease; +} + +.btn:active { + transform: translateY(1px); +} + +.btn--primary { + background: var(--accent); + color: #0b0d12; + background: linear-gradient(135deg, var(--python-yellow), #ffb703); + color: #1b1400; +} + +.btn--primary:hover { + opacity: 0.92; +} + +.btn--secondary { + background: var(--python-blue); + color: #fff; +} + +.btn--secondary:hover { + opacity: 0.9; +} + +.btn--ghost { + background: transparent; + border-color: var(--border); + color: var(--text); +} + +.btn--ghost:hover { + background: var(--surface-hover); +} + +.btn--sm { + padding: 0.4rem 0.8rem; + font-size: 0.8rem; +} + +.btn--block { + width: 100%; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); +} + +.container { + max-width: 1080px; + margin: 0 auto; + padding: 2rem 1.5rem; + width: 100%; +} + +.badge { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.2rem 0.6rem; + border-radius: 999px; + background: var(--surface-hover); + border: 1px solid var(--border); + font-size: 0.72rem; + font-weight: 600; + color: var(--text-muted); +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 999px; +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..615f018 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { HashRouter } from 'react-router-dom' +import App from './App' +import { AuthProvider } from './context/AuthContext' +import { ProgressProvider } from './context/ProgressContext' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + + + + + + + , +) diff --git a/src/pages/Auth.css b/src/pages/Auth.css new file mode 100644 index 0000000..e29cb81 --- /dev/null +++ b/src/pages/Auth.css @@ -0,0 +1,84 @@ +.auth { + display: flex; + justify-content: center; + padding: 3rem 1.5rem; + flex: 1; +} + +.auth__card { + width: 100%; + max-width: 420px; + padding: 2rem; +} + +.auth__notice { + background: rgba(75, 139, 190, 0.12); + border: 1px solid rgba(75, 139, 190, 0.35); + color: var(--text-muted); + font-size: 0.78rem; + padding: 0.6rem 0.8rem; + border-radius: 8px; + margin-bottom: 1.25rem; +} + +.auth__card h1 { + margin: 0 0 0.4rem; + font-size: 1.5rem; +} + +.auth__sub { + color: var(--text-muted); + font-size: 0.88rem; + margin-bottom: 1.5rem; +} + +.auth__form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.auth__form label { + display: flex; + flex-direction: column; + gap: 0.4rem; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-muted); +} + +.auth__form input { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.6rem 0.75rem; + color: var(--text); + font-size: 0.9rem; +} + +.auth__form input:focus { + outline: none; + border-color: var(--accent); +} + +.auth__error { + background: rgba(220, 80, 80, 0.12); + border: 1px solid rgba(220, 80, 80, 0.4); + color: #ff8a8a; + padding: 0.5rem 0.75rem; + border-radius: 8px; + font-size: 0.82rem; +} + +.auth__switch { + margin-top: 1rem; + font-size: 0.85rem; + color: var(--text-muted); + text-align: center; +} + +.auth__switch a { + color: var(--accent); + font-weight: 600; + text-decoration: none; +} diff --git a/src/pages/CertificatePage.css b/src/pages/CertificatePage.css new file mode 100644 index 0000000..517f848 --- /dev/null +++ b/src/pages/CertificatePage.css @@ -0,0 +1,79 @@ +.certificate-page { + max-width: 1080px; + margin: 0 auto; + padding: 2rem 1.5rem 3rem; + width: 100%; +} + +.certificate-page__intro { + max-width: 680px; + margin: 0 auto 2rem; + text-align: center; +} + +.certificate-page__intro p { + color: var(--text-muted); +} + +.certificate-page__progress { + color: var(--text-faint) !important; + font-size: 0.85rem; +} + +.certificate-page__grid { + display: grid; + grid-template-columns: 300px 1fr; + gap: 1.5rem; + align-items: start; +} + +.certificate-page__form { + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; + position: sticky; + top: 84px; +} + +.certificate-page__form label { + display: flex; + flex-direction: column; + gap: 0.4rem; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-muted); +} + +.certificate-page__form input { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.6rem 0.75rem; + color: var(--text); + font-size: 0.9rem; +} + +.certificate-page__form input:focus { + outline: none; + border-color: var(--accent); +} + +.certificate-page__hint { + font-size: 0.78rem; + color: var(--text-faint); + margin: 0; +} + +.certificate-page__preview { + padding: 1rem; +} + +@media (max-width: 800px) { + .certificate-page__grid { + grid-template-columns: 1fr; + } + .certificate-page__form { + position: static; + } +} diff --git a/src/pages/CertificatePage.tsx b/src/pages/CertificatePage.tsx new file mode 100644 index 0000000..4d4754e --- /dev/null +++ b/src/pages/CertificatePage.tsx @@ -0,0 +1,89 @@ +import { useRef, useState } from 'react' +import { useAuth } from '../context/AuthContext' +import { useProgress } from '../context/ProgressContext' +import Certificate from '../components/Certificate' +import './CertificatePage.css' + +function todayLabel() { + return new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }) +} + +export default function CertificatePage() { + const { user } = useAuth() + const { completedCount, percentComplete } = useProgress() + const [name, setName] = useState(user?.name ?? '') + const svgRef = useRef(null) + + function downloadPng() { + const svg = svgRef.current + if (!svg) return + + const serializer = new XMLSerializer() + const svgString = serializer.serializeToString(svg) + const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }) + const url = URL.createObjectURL(svgBlob) + + const img = new Image() + img.onload = () => { + const canvas = document.createElement('canvas') + canvas.width = 2000 + canvas.height = 1400 + const ctx = canvas.getContext('2d') + if (!ctx) return + ctx.drawImage(img, 0, 0, canvas.width, canvas.height) + URL.revokeObjectURL(url) + + canvas.toBlob((blob) => { + if (!blob) return + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + const fileName = (name.trim() || 'certificate').replace(/\s+/g, '_').toLowerCase() + link.download = `python-30-days-${fileName}.png` + link.click() + }, 'image/png') + } + img.src = url + } + + return ( +
+
+

Your certificate

+

+ Enter the name you would like printed on your certificate and download it. No account and no + quiz score is required, this is yours whenever you feel ready to claim it. +

+ {completedCount > 0 && ( +

+ For reference, you have personally marked {completedCount} of 30 days complete ({percentComplete}%). +

+ )} +
+ +
+
+ + +

+ The certificate updates live as you type. Download it as a PNG image you can print, share or + attach to a resume or portfolio. +

+
+ +
+ +
+
+
+ ) +} diff --git a/src/pages/DayPage.css b/src/pages/DayPage.css new file mode 100644 index 0000000..b94dc04 --- /dev/null +++ b/src/pages/DayPage.css @@ -0,0 +1,184 @@ +.day-page { + max-width: 860px; + margin: 0 auto; + padding: 1.75rem 1.5rem 3rem; + width: 100%; +} + +.day-page__topbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.25rem; + font-size: 0.85rem; + color: var(--text-faint); +} + +.day-page__crumbs { + display: flex; + gap: 0.4rem; +} + +.day-page__crumbs a { + color: var(--text-muted); + text-decoration: none; +} + +.day-page__crumbs a:hover { + color: var(--accent); +} + +.day-page__lesson { + padding: 2rem; + margin-bottom: 1.5rem; +} + +.day-page__lesson h1 { + margin-top: 0; + font-size: 1.8rem; +} + +.day-page__lesson h2 { + font-size: 1.3rem; + margin-top: 2rem; + border-bottom: 1px solid var(--border); + padding-bottom: 0.4rem; +} + +.day-page__lesson h3 { + font-size: 1.05rem; + margin-top: 1.5rem; +} + +.day-page__lesson table { + width: 100%; + border-collapse: collapse; + margin: 1rem 0; + font-size: 0.9rem; +} + +.day-page__lesson th, +.day-page__lesson td { + border: 1px solid var(--border); + padding: 0.5rem 0.75rem; + text-align: left; +} + +.day-page__lesson th { + background: var(--surface-hover); +} + +.day-page__lesson hr { + border: none; + border-top: 1px solid var(--border); + margin: 1.75rem 0; +} + +.day-page__lesson blockquote { + border-left: 3px solid var(--accent); + margin: 1rem 0; + padding: 0.4rem 1rem; + color: var(--text-muted); + background: var(--surface-hover); + border-radius: 0 8px 8px 0; +} + +.day-page__lesson .inline-code { + background: var(--surface-hover); + border: 1px solid var(--border); + border-radius: 5px; + padding: 0.1rem 0.35rem; + font-size: 0.85em; +} + +.day-page__lesson ul, +.day-page__lesson ol { + padding-left: 1.4rem; +} + +.day-page__lesson li { + margin-bottom: 0.3rem; +} + +.day-page__solution { + padding: 1.5rem 1.5rem 0.5rem; + margin-bottom: 1.5rem; +} + +.day-page__solution-head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.day-page__solution-head h2 { + margin: 0; + font-size: 1.1rem; +} + +.day-page__complete { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1.25rem 1.5rem; + margin-bottom: 2rem; +} + +.day-page__complete p { + margin: 0.25rem 0 0; + color: var(--text-faint); + font-size: 0.82rem; + max-width: 480px; +} + +.day-page__pagination { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.day-page__nav-btn { + display: flex; + flex-direction: column; + gap: 0.3rem; + padding: 1rem 1.25rem; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--surface); + text-decoration: none; + color: var(--text); + transition: border-color 0.15s ease; +} + +.day-page__nav-btn:hover { + border-color: var(--accent); +} + +.day-page__nav-btn--next { + text-align: right; + align-items: flex-end; +} + +.day-page__nav-label { + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-faint); +} + +.day-page__nav-title { + font-weight: 600; + font-size: 0.92rem; +} + +@media (max-width: 600px) { + .day-page__pagination { + grid-template-columns: 1fr; + } + .day-page__complete { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/src/pages/DayPage.tsx b/src/pages/DayPage.tsx new file mode 100644 index 0000000..929eb3f --- /dev/null +++ b/src/pages/DayPage.tsx @@ -0,0 +1,103 @@ +import { useEffect } from 'react' +import { Link, Navigate, useParams } from 'react-router-dom' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { getAdjacentDays, getDayMeta, TOTAL_DAYS } from '../data/curriculum' +import { getLessonMarkdown, getSolutionCode } from '../data/content' +import { useProgress } from '../context/ProgressContext' +import CodeBlock from '../components/CodeBlock' +import './DayPage.css' + +export default function DayPage() { + const { dayId } = useParams<{ dayId: string }>() + const id = Number(dayId) + const meta = getDayMeta(id) + const { isComplete, toggleComplete } = useProgress() + + useEffect(() => { + window.scrollTo({ top: 0, behavior: 'smooth' }) + }, [id]) + + if (!meta || Number.isNaN(id)) { + return + } + + const markdown = getLessonMarkdown(meta.slug) + const solution = getSolutionCode(meta.slug) + const { prev, next } = getAdjacentDays(id) + const done = isComplete(id) + + return ( +
+
+
+ Home + / + Day {String(id).padStart(2, '0')} +
+
+ Day {id} of {TOTAL_DAYS} +
+
+ +
+ + {markdown} + +
+ + {solution && ( +
+
+

Project Solution

+ solution.py +
+ {solution} +
+ )} + +
+
+ {done ? 'Marked complete' : 'Mark this day complete'} +

+ This is just a personal tracker stored in your browser. It never blocks navigation, jump to + any day whenever you like. +

+
+ +
+ + +
+ ) +} diff --git a/src/pages/Home.css b/src/pages/Home.css new file mode 100644 index 0000000..ef099f2 --- /dev/null +++ b/src/pages/Home.css @@ -0,0 +1,164 @@ +.home { + padding: 0; +} + +.hero { + padding: 4rem 1.5rem 3rem; + max-width: 860px; + margin: 0 auto; + text-align: center; +} + +.hero__badges { + display: flex; + justify-content: center; + gap: 0.5rem; + flex-wrap: wrap; + margin-bottom: 1.25rem; +} + +.hero h1 { + font-size: clamp(2.2rem, 5vw, 3.2rem); + margin: 0 0 1rem; +} + +.hero__accent { + background: linear-gradient(135deg, var(--python-blue), var(--python-yellow)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.hero__lead { + color: var(--text-muted); + font-size: 1.1rem; + max-width: 640px; + margin: 0 auto 1.75rem; +} + +.hero__actions { + display: flex; + justify-content: center; + gap: 1rem; + margin-bottom: 1.25rem; + flex-wrap: wrap; +} + +.hero__note { + color: var(--text-faint); + font-size: 0.85rem; + max-width: 520px; + margin: 0 auto; +} + +.stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; + max-width: 1080px; + margin: 0 auto 3rem; + padding: 0 1.5rem; +} + +.stats__card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.25rem 1rem; + text-align: center; +} + +.stats__value { + display: block; + font-size: 1.8rem; + font-weight: 800; + color: var(--accent-strong); +} + +.stats__label { + font-size: 0.78rem; + color: var(--text-muted); +} + +.curriculum-preview { + max-width: 1080px; + margin: 0 auto; + padding: 0 1.5rem 3rem; +} + +.curriculum-preview__head { + text-align: center; + margin-bottom: 2rem; +} + +.curriculum-preview__head p { + color: var(--text-muted); +} + +.curriculum-topic { + margin-bottom: 2rem; +} + +.curriculum-topic h3 { + font-size: 0.95rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-faint); + margin-bottom: 0.9rem; +} + +.curriculum-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 0.9rem; +} + +.day-card { + display: block; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem; + text-decoration: none; + color: var(--text); + transition: border-color 0.15s ease, transform 0.15s ease; +} + +.day-card:hover { + border-color: var(--accent); + transform: translateY(-2px); +} + +.day-card__top { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.day-card__number { + font-size: 0.72rem; + font-weight: 700; + color: var(--text-faint); + letter-spacing: 0.04em; +} + +.day-card__done { + font-size: 0.68rem; + font-weight: 700; + color: var(--success); + background: rgba(76, 175, 125, 0.12); + padding: 0.1rem 0.5rem; + border-radius: 999px; +} + +.day-card__title { + font-weight: 600; + font-size: 0.95rem; +} + +@media (max-width: 700px) { + .stats { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx new file mode 100644 index 0000000..712383e --- /dev/null +++ b/src/pages/Home.tsx @@ -0,0 +1,85 @@ +import { Link } from 'react-router-dom' +import { CURRICULUM, TOPICS } from '../data/curriculum' +import { useProgress } from '../context/ProgressContext' +import './Home.css' + +export default function Home() { + const { isComplete, completedCount, percentComplete } = useProgress() + + return ( +
+
+
+ 30 Lessons + 30 Projects + No Login Required + Free Forever +
+

+ Learn Python in 30 Days +

+

+ A complete, beginner friendly Python course. Every day pairs a focused lesson with a hands on + project so you learn by writing real code, not by memorizing syntax. +

+
+ + Start Day 1 + + + Get your certificate + +
+

+ Browse every lesson freely, jump between days in any order, and generate your completion + certificate whenever you like. An account is never required. +

+
+ +
+
+ 30 + Daily lessons +
+
+ 30 + Hands on projects +
+
+ {completedCount} + Days you have marked complete +
+
+ {percentComplete}% + Your progress +
+
+ +
+
+

Curriculum

+

Every day builds on the last, but nothing is locked. Jump anywhere, anytime.

+
+ + {TOPICS.map((topic) => ( +
+

{topic}

+
+ {CURRICULUM.filter((d) => d.topic === topic).map((day) => ( + +
+ Day {String(day.id).padStart(2, '0')} + {isComplete(day.id) && Done} +
+
+ {day.emoji} {day.title} +
+ + ))} +
+
+ ))} +
+
+ ) +} diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx new file mode 100644 index 0000000..702eb44 --- /dev/null +++ b/src/pages/Login.tsx @@ -0,0 +1,72 @@ +import { FormEvent, useState } from 'react' +import { Link, Navigate, useNavigate } from 'react-router-dom' +import { useAuth } from '../context/AuthContext' +import './Auth.css' + +export default function Login() { + const { user, login } = useAuth() + const navigate = useNavigate() + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + + if (user) return + + function handleSubmit(e: FormEvent) { + e.preventDefault() + const result = login(email, password) + if (!result.ok) { + setError(result.error ?? 'Could not log in.') + return + } + navigate('/') + } + + return ( +
+
+
+ This is a showcase login. All lessons and the certificate are available without an account. +
+

Welcome back

+

Log in to see a personalized greeting. Nothing else changes.

+ +
+ + + + {error &&
{error}
} + + +
+ +

+ Do not have a showcase account? Sign up +

+

+ Just here to learn? Skip straight to Day 1 +

+
+
+ ) +} diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx new file mode 100644 index 0000000..2a4591b --- /dev/null +++ b/src/pages/NotFound.tsx @@ -0,0 +1,15 @@ +import { Link } from 'react-router-dom' + +export default function NotFound() { + return ( +
+

404

+

+ This page does not exist. Maybe it wandered off during a while loop. +

+ + Back to home + +
+ ) +} diff --git a/src/pages/Signup.tsx b/src/pages/Signup.tsx new file mode 100644 index 0000000..3314149 --- /dev/null +++ b/src/pages/Signup.tsx @@ -0,0 +1,84 @@ +import { FormEvent, useState } from 'react' +import { Link, Navigate, useNavigate } from 'react-router-dom' +import { useAuth } from '../context/AuthContext' +import './Auth.css' + +export default function Signup() { + const { user, signup } = useAuth() + const navigate = useNavigate() + const [name, setName] = useState('') + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState(null) + + if (user) return + + function handleSubmit(e: FormEvent) { + e.preventDefault() + const result = signup(name, email, password) + if (!result.ok) { + setError(result.error ?? 'Could not create an account.') + return + } + navigate('/') + } + + return ( +
+
+
+ This is a showcase signup. Accounts are stored only in your browser, there is no server or + database. Every lesson and the certificate work without signing up. +
+

Create an account

+

Purely optional. It only unlocks a personalized greeting in the navbar.

+ +
+ + + + + {error &&
{error}
} + + +
+ +

+ Already have a showcase account? Log in +

+

+ Just here to learn? Skip straight to Day 1 +

+
+
+ ) +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d8f6798 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ccb7786 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// Repository name used for the GitHub Pages sub-path. +// GitHub Pages serves project sites from https://.github.io// +// Override with VITE_BASE_PATH env var if the repo name differs. +const REPO_NAME = 'python-30-days-react' + +export default defineConfig(({ mode }) => ({ + plugins: [react()], + base: mode === 'production' ? `/${REPO_NAME}/` : '/', + build: { + outDir: 'dist', + sourcemap: false, + }, +})) From f9bce612d4e79e8c8e1a1a4af335a0f7538b495b Mon Sep 17 00:00:00 2001 From: Reza Hassani Date: Mon, 6 Jul 2026 14:11:49 +0330 Subject: [PATCH 4/6] Create deploy.yml --- .github/workflows/deploy.yml | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..bc54f90 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,53 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: {} + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Build production bundle + run: npm run build + + - name: Upload build artifact + uses: actions/upload-pages-artifact@v3 + with: + path: dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From ada5249ae40e30946733f2a128e2e3d281e4bf8a Mon Sep 17 00:00:00 2001 From: Reza Hassani Date: Mon, 6 Jul 2026 14:12:08 +0330 Subject: [PATCH 5/6] Add CI workflow for linting and building --- .github/workflows/ci.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..70a9252 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches-ignore: [main] + +jobs: + lint-and-build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Type check and build + run: npm run build From 138b2e17def0d18d47654e6fbf43a39026706387 Mon Sep 17 00:00:00 2001 From: Reza Hassani Date: Mon, 6 Jul 2026 14:12:33 +0330 Subject: [PATCH 6/6] Create .eslintrc.cjs --- .eslintrc.cjs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .eslintrc.cjs diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..f18c49c --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,22 @@ +module.exports = { + root: true, + env: { browser: true, es2021: true, node: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { jsx: true }, + }, + plugins: ['@typescript-eslint', 'react-refresh'], + ignorePatterns: ['dist', '.eslintrc.cjs', 'vite.config.ts'], + rules: { + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + }, +}