diff --git a/SECURITY.md b/SECURITY.md index 5a9bdcc..7ea7406 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ If you discover a security issue in a Fynes Forge project, please report it responsibly: -1. Email **tf.dev@icloud.com** with the subject line: `[SECURITY] ` +1. Email **contact@fynesforge.dev** with the subject line: `[SECURITY] ` 2. Include: a description of the vulnerability, steps to reproduce, and the potential impact 3. You will receive a response within 5 business days diff --git a/git_101/Grade 2/git_log.md b/git_101/Grade 2/git_log.md index 8a80752..7c59da4 100644 --- a/git_101/Grade 2/git_log.md +++ b/git_101/Grade 2/git_log.md @@ -29,13 +29,13 @@ git log Output: ```yaml commit a3f6f1c9e29a3e82a4dcb3f59e8ab3d28a12c9af -Author: Tom Fynes +Author: Tom Fynes Date: Fri May 24 14:15 2025 Add styling to navigation bar commit d1e8f91b45e8e1f9e4aa84b2e1830c4b5d674cd0 -Author: Tom Fynes +Author: Tom Fynes Date: Fri May 24 13:50 2025 Initial commit: add version.txt diff --git a/python_101/Getting Started/Linux.md b/python_101/Getting Started/Linux.md new file mode 100644 index 0000000..3a681f6 --- /dev/null +++ b/python_101/Getting Started/Linux.md @@ -0,0 +1,75 @@ +--- +sidebar_position: 3 +--- + +# Configure Python on Linux + +This page will walk you through installing Python and setting up your environment on Linux. Commands below use `apt` (Debian/Ubuntu). Adjust for your distribution (`dnf`, `yum`, `pacman`) as needed. + +## Installing Python + +Most Linux distributions include Python 3, but it may not be the latest version. + +```bash +sudo apt update +sudo apt install python3 python3-pip python3-venv -y +``` + +Verify: + +```bash +python3 --version +pip3 --version +``` + +## Installing VS Code + +### Option A: Snap + +```bash +sudo snap install code --classic +``` + +### Option B: .deb Package + +Download the `.deb` from [code.visualstudio.com](https://code.visualstudio.com/) and run: + +```bash +sudo dpkg -i code_*.deb +``` + +Once open, install the **Python** extension (Ctrl+Shift+X → search Python → Install). + +## Creating the Course Folder + +```bash +mkdir python-101 +cd python-101 +``` + +## Setting Up a Virtual Environment + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +You should now see `(.venv)` in your terminal prompt. + +> You will need to run `source .venv/bin/activate` each time you open a new terminal. Grade 8 covers virtual environments in detail. + +## Verifying Your Setup + +With your virtual environment active: + +```bash +python --version +pip --version +``` + +Both should return version numbers. You are ready to start Grade 1. + +### Notes + +* Once inside a virtual environment, `python` and `pip` work without the `3` suffix. +* Some distributions may require `python3-full` instead of just `python3-venv` — check the error message if `venv` creation fails. diff --git a/python_101/Getting Started/Windows.md b/python_101/Getting Started/Windows.md new file mode 100644 index 0000000..9ed4329 --- /dev/null +++ b/python_101/Getting Started/Windows.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 1 +--- + +# Configure Python on Windows + +This page will walk you through installing Python and setting up your environment on Windows. + +## Installing Python + +1. Go to [python.org/downloads](https://www.python.org/downloads/) and download the latest Python 3 installer. +2. Run the installer. +3. **Important:** On the first screen, tick **"Add Python to PATH"** before clicking Install Now. +4. Once complete, open **Command Prompt** and verify the installation: + +```bash +python --version +``` + +> If you see `python` is not recognised, restart your terminal and try again. If it still fails, check that Python was added to your PATH via **System Properties → Environment Variables**. + +## Installing VS Code + +1. Download [VS Code](https://code.visualstudio.com/). +2. Install and open it. +3. Go to the Extensions panel (Ctrl+Shift+X) and search for **Python** — install the extension by Microsoft. + +## Creating the Course Folder + +Open Command Prompt and run: + +```bash +mkdir python-101 +cd python-101 +``` + +## Setting Up a Virtual Environment + +It is good practice to use a virtual environment for every project. This keeps your installed packages separate from the global Python installation. + +```bash +python -m venv .venv +.venv\Scripts\activate +``` + +You should now see `(.venv)` in your terminal prompt. + +> You will need to run `.venv\Scripts\activate` each time you open a new terminal. Grade 8 covers virtual environments in detail. + +## Verifying Your Setup + +With your virtual environment active, run: + +```bash +python --version +pip --version +``` + +Both should return version numbers. You are ready to start Grade 1. + +### Notes + +* On Windows, `python` and `pip` refer to the version installed in your active environment. +* If `python` is not found but `python3` works, use `python3` throughout the course. +* VS Code will usually detect your `.venv` automatically — if prompted to select an interpreter, choose the one inside `.venv`. diff --git a/python_101/Getting Started/macOS.md b/python_101/Getting Started/macOS.md new file mode 100644 index 0000000..4e05d0a --- /dev/null +++ b/python_101/Getting Started/macOS.md @@ -0,0 +1,79 @@ +--- +sidebar_position: 2 +--- + +# Configure Python on macOS + +This page will walk you through installing Python and setting up your environment on macOS. + +## Installing Python + +macOS comes with a system Python, but it is outdated and should not be used for development. Install a fresh version using one of the following methods. + +### Option A: Homebrew (Recommended) + +If you do not have Homebrew installed: + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +Then install Python: + +```bash +brew install python +``` + +### Option B: Official Installer + +Download the macOS installer from [python.org/downloads](https://www.python.org/downloads/) and follow the prompts. + +### Verify + +```bash +python3 --version +``` + +> On macOS, use `python3` and `pip3` unless you have configured an alias or are inside a virtual environment. + +## Installing VS Code + +1. Download [VS Code](https://code.visualstudio.com/). +2. Move it to your Applications folder. +3. Open it and install the **Python** extension (Cmd+Shift+X → search Python → Install). + +## Creating the Course Folder + +Open Terminal and run: + +```bash +mkdir python-101 +cd python-101 +``` + +## Setting Up a Virtual Environment + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +You should now see `(.venv)` in your terminal prompt. + +> You will need to run `source .venv/bin/activate` each time you open a new terminal. Grade 8 covers virtual environments in detail. + +## Verifying Your Setup + +With your virtual environment active, run: + +```bash +python --version +pip --version +``` + +Both should return version numbers. You are ready to start Grade 1. + +### Notes + +* Once inside a virtual environment, `python` and `pip` refer to the environment's versions — no need to use `python3`. +* VS Code will detect your `.venv` automatically when you open the folder — select it as your interpreter if prompted. diff --git a/python_101/Grade 1/01 What is Python.md b/python_101/Grade 1/01 What is Python.md new file mode 100644 index 0000000..9f20176 --- /dev/null +++ b/python_101/Grade 1/01 What is Python.md @@ -0,0 +1,39 @@ +# What is Python + +**Python** is a high-level, general-purpose programming language known for its clean and readable syntax. It was created by Guido van Rossum and first released in 1991, and has since grown into one of the most widely used languages in the world. + +### Why Python? + +- **Readable**: Python code reads almost like plain English, which makes it easier to learn and maintain. +- **Versatile**: Python is used across web development, automation, data engineering, data science, machine learning, and more. +- **Large Ecosystem**: There are thousands of libraries and packages available to extend what Python can do out of the box. +- **In Demand**: Python consistently ranks as one of the most sought-after skills in data and engineering roles. + +### Python in Data Roles + +For data engineers, analysts, and scientists, Python is a core tool. It is used to: + +- Read, clean, and transform data +- Automate repetitive tasks and pipelines +- Interact with APIs and databases +- Build and train machine learning models +- Create visualisations and reports + +### Python Versions + +Python 2 reached end-of-life in 2020 and should not be used. This course uses **Python 3**, specifically 3.10 or higher. If you see `python` and `python3` used interchangeably, they refer to the same thing — the difference is covered in the setup guides. + +### How Python Runs + +Python is an **interpreted** language. This means code is executed line by line at runtime, rather than compiled into a binary first. You write a `.py` file, hand it to the Python interpreter, and it runs. + +``` +your_script.py → Python interpreter → output +``` + +This makes Python quick to write and test, at the cost of being slower than compiled languages like C or Go for heavy computation. In practice, this rarely matters for the work covered in this course. + +### Practice Exercises + +* In your own words, write down one reason why Python is popular in data engineering. +* Look up one Python library used in data work that interests you and note what it does. diff --git a/python_101/Grade 1/02 Your first script.md b/python_101/Grade 1/02 Your first script.md new file mode 100644 index 0000000..136f7e8 --- /dev/null +++ b/python_101/Grade 1/02 Your first script.md @@ -0,0 +1,93 @@ +# Your First Script + +### Introduction +A Python script is simply a text file with a `.py` extension. The Python interpreter reads and executes it from top to bottom. This section covers how to write, save, and run your first script. + +### Hello, World + +The traditional first program in any language — print a message to the screen. + +#### In the Terminal (Interactive Mode) + +Python comes with an interactive shell called the REPL (Read-Eval-Print Loop). You can use it to run single lines of code immediately. + +```bash +python +``` + +```python +>>> print("Hello, World!") +Hello, World! +``` + +Type `exit()` or press `Ctrl+D` to leave the REPL. + +#### As a Script File + +Create a file called `hello.py` and add the following: + +```python +print("Hello, World!") +``` + +Run it from the terminal: + +```bash +python hello.py +``` + +**Output** + +``` +Hello, World! +``` + +### Comments + +Comments are lines Python ignores — they are there for the reader, not the interpreter. Use them to explain what your code is doing. + +```python +# This is a single-line comment +print("Hello!") # This comment is at the end of a line + +""" +This is a multi-line string. +It is often used as a block comment or docstring. +""" +``` + +### The print() Function + +`print()` outputs values to the terminal. It can print strings, numbers, variables, and more. + +```python +print("Hello, World!") +print(42) +print("The answer is", 42) +print("Line one\nLine two") # \n creates a new line +``` + +**Output** + +``` +Hello, World! +42 +The answer is 42 +Line one +Line two +``` + +### Running Scripts vs the REPL + +| | REPL | Script file | +|--|------|-------------| +| Good for | Quick tests, exploring | Real programs, saving work | +| How to run | Type `python` | `python filename.py` | +| Output shown automatically | ✅ | Only via `print()` | + +### Practice Exercises + +* Create a file called `grade1.py` and print your name to the terminal. +* Add a comment above the print statement explaining what it does. +* Print three separate lines of output using three `print()` calls. +* Print two values in a single `print()` call separated by a space. diff --git a/python_101/Grade 1/03 Variables and data types.md b/python_101/Grade 1/03 Variables and data types.md new file mode 100644 index 0000000..c5617a5 --- /dev/null +++ b/python_101/Grade 1/03 Variables and data types.md @@ -0,0 +1,115 @@ +# Variables and Data Types + +### Introduction +Variables are used to store values so you can refer to them later in your code. Python is dynamically typed, which means you do not need to declare a type — Python works it out from the value you assign. + +### Assigning Variables + +```python +name = "Alice" +age = 30 +salary = 75000.50 +is_active = True +``` + +Variable names should be lowercase, with words separated by underscores. This is called **snake_case** and is the Python convention. + +### Core Data Types + +#### String (`str`) +Text data, enclosed in single or double quotes. + +```python +firstname = "Alice" +greeting = 'Hello, World!' +``` + +You can combine strings using `+`: + +```python +full_name = "Alice" + " " + "Smith" +print(full_name) # Alice Smith +``` + +Or use an **f-string** (the preferred modern approach): + +```python +firstname = "Alice" +lastname = "Smith" +print(f"Full name: {firstname} {lastname}") # Full name: Alice Smith +``` + +#### Integer (`int`) +Whole numbers, positive or negative. + +```python +age = 30 +year = 2024 +negative = -10 +``` + +#### Float (`float`) +Numbers with a decimal point. + +```python +salary = 75000.50 +temperature = -3.5 +``` + +#### Boolean (`bool`) +One of two values: `True` or `False`. Note the capital letter. + +```python +is_active = True +has_contract = False +``` + +#### NoneType (`None`) +Represents the absence of a value. + +```python +contract_end_date = None +``` + +### Checking the Type of a Variable + +```python +name = "Alice" +age = 30 +salary = 75000.50 + +print(type(name)) # +print(type(age)) # +print(type(salary)) # +``` + +### Type Conversion + +You can convert between types when needed. + +```python +age_str = "30" +age_int = int(age_str) # "30" → 30 +price_str = str(19.99) # 19.99 → "19.99" +whole = int(3.9) # 3.9 → 3 (truncates, does not round) +``` + +### Common Mistakes + +* Forgetting quotes around strings: `name = Alice` → `NameError` +* Mixing types without converting: `"Age: " + 30` → `TypeError` + +```python +# Correct way +age = 30 +print("Age: " + str(age)) +# Or, more cleanly: +print(f"Age: {age}") +``` + +### Practice Exercises + +* Create variables for your `firstname`, `lastname`, `age`, and `salary`. Print them all on one line using an f-string. +* Create a variable with the value `"42"`. Convert it to an integer and add `8` to it. Print the result. +* Print the type of each of the four variables you created. +* Create a variable called `is_employed` and set it to `True`. Print it. diff --git a/python_101/Grade 1/04 User input and print.md b/python_101/Grade 1/04 User input and print.md new file mode 100644 index 0000000..a1d4fe3 --- /dev/null +++ b/python_101/Grade 1/04 User input and print.md @@ -0,0 +1,92 @@ +# User Input and Print + +### Introduction +Programs often need to communicate with the person running them — either by displaying output or asking for input. This section covers the `print()` function in more detail and introduces `input()` for reading user input. + +### print() in Depth + +`print()` can take multiple arguments and has optional keyword arguments to control formatting. + +```python +print("Hello", "World") # Hello World +print("Hello", "World", sep="-") # Hello-World +print("Loading", end="...") # Loading... (no newline) +print("done") # done +``` + +**Formatting numbers** + +```python +salary = 75234.5678 +print(f"Salary: {salary:.2f}") # Salary: 75234.57 +print(f"Salary: {salary:,.2f}") # Salary: 75,234.57 +``` + +### input() + +`input()` pauses the program and waits for the user to type something and press Enter. It always returns a **string**. + +#### Syntax + +```python +variable = input("Prompt message: ") +``` + +**Example** + +```python +name = input("Enter your name: ") +print(f"Hello, {name}!") +``` + +``` +Enter your name: Alice +Hello, Alice! +``` + +### input() Always Returns a String + +If you need a number from the user, you must convert it. + +```python +age_input = input("Enter your age: ") +age = int(age_input) +print(f"In 10 years you will be {age + 10}") +``` + +Or on one line: + +```python +age = int(input("Enter your age: ")) +``` + +> If the user types something that cannot be converted (e.g. `"hello"` when you call `int()`), Python will raise a `ValueError`. Error handling is covered in Grade 6. + +### Combining input() and print() + +```python +firstname = input("First name: ") +lastname = input("Last name: ") +salary = float(input("Salary: ")) + +print(f"\nEmployee Summary") +print(f"Name: {firstname} {lastname}") +print(f"Salary: £{salary:,.2f}") +``` + +``` +First name: Alice +Last name: Smith +Salary: 75000 + +Employee Summary +Name: Alice Smith +Salary: £75,000.00 +``` + +### Practice Exercises + +* Ask the user for their name and favourite number. Print a sentence using both. +* Ask the user for two numbers. Add them together and print the result. +* Print a formatted receipt: ask for an item name and price, then print them aligned neatly using an f-string. +* Ask the user for their year of birth. Calculate and print their approximate age. diff --git a/python_101/Grade 2/01 Operators.md b/python_101/Grade 2/01 Operators.md new file mode 100644 index 0000000..b0172ed --- /dev/null +++ b/python_101/Grade 2/01 Operators.md @@ -0,0 +1,85 @@ +# Comparison and Logical Operators + +### Introduction +Operators let you compare values and combine conditions. The results are always `True` or `False`, making them essential for controlling the flow of your programs. + +### Comparison Operators + +These compare two values and return a boolean. + +| Operator | Meaning | Example | Result | +|----------|---------|---------|--------| +| `==` | Equal to | `5 == 5` | `True` | +| `!=` | Not equal to | `5 != 3` | `True` | +| `>` | Greater than | `10 > 5` | `True` | +| `>=` | Greater than or equal to | `5 >= 5` | `True` | +| `<` | Less than | `3 < 10` | `True` | +| `<=` | Less than or equal to | `4 <= 3` | `False` | + +**Examples** + +```python +salary = 60000 + +print(salary > 50000) # True +print(salary == 60000) # True +print(salary != 60000) # False +print(salary < 40000) # False +``` + +### Logical Operators + +Logical operators combine multiple conditions. + +| Operator | Meaning | Example | Result | +|----------|---------|---------|--------| +| `and` | Both must be True | `True and False` | `False` | +| `or` | At least one must be True | `True or False` | `True` | +| `not` | Inverts the result | `not True` | `False` | + +**Examples** + +```python +salary = 60000 +department = "Engineering" + +print(salary > 50000 and department == "Engineering") # True +print(salary > 80000 or department == "Engineering") # True +print(not salary > 80000) # True +``` + +### Membership Operators + +Check whether a value exists in a sequence. + +| Operator | Meaning | Example | Result | +|----------|---------|---------|--------| +| `in` | Value is in the sequence | `"Alice" in ["Alice", "Bob"]` | `True` | +| `not in` | Value is not in the sequence | `5 not in [1, 2, 3]` | `True` | + +```python +approved_departments = ["Engineering", "Finance", "HR"] +department = "Engineering" + +print(department in approved_departments) # True +print("Marketing" not in approved_departments) # True +``` + +### Arithmetic Operators + +| Operator | Meaning | Example | Result | +|----------|---------|---------|--------| +| `+` | Addition | `10 + 5` | `15` | +| `-` | Subtraction | `10 - 5` | `5` | +| `*` | Multiplication | `10 * 5` | `50` | +| `/` | Division | `10 / 3` | `3.333...` | +| `//` | Floor division | `10 // 3` | `3` | +| `%` | Modulo (remainder) | `10 % 3` | `1` | +| `**` | Exponent | `2 ** 8` | `256` | + +### Practice Exercises + +* Check whether a salary of `55000` is greater than `50000` and less than `100000`. Print the result. +* Create two variables `is_employed` and `has_contract`. Set one to `True` and one to `False`. Print whether both are `True`, and whether at least one is `True`. +* Check if the string `"python"` is in the list `["sql", "python", "java"]`. +* Calculate the remainder when `17` is divided by `5`. diff --git a/python_101/Grade 2/02 If statements.md b/python_101/Grade 2/02 If statements.md new file mode 100644 index 0000000..4662d8f --- /dev/null +++ b/python_101/Grade 2/02 If statements.md @@ -0,0 +1,112 @@ +# If Statements + +### Introduction +If statements allow your program to make decisions. Code inside an `if` block only runs when the condition is `True`. + +### Basic If Statement + +```python +if condition: + # code to run when condition is True +``` + +**Example** + +```python +salary = 75000 + +if salary > 50000: + print("Senior role") +``` + +### If / Else + +```python +salary = 30000 + +if salary > 50000: + print("Senior role") +else: + print("Junior role") +``` + +### If / Elif / Else + +Use `elif` (short for "else if") to check multiple conditions in sequence. Python stops at the first match. + +```python +salary = 55000 + +if salary < 30000: + print("Junior") +elif salary < 60000: + print("Mid-level") +elif salary < 100000: + print("Senior") +else: + print("Principal or above") +``` + +**Output** + +``` +Mid-level +``` + +### Indentation Matters + +Python uses indentation (4 spaces) to define blocks. Incorrect indentation will cause an `IndentationError`. + +```python +# Correct +if salary > 50000: + print("High earner") + +# Wrong — will raise IndentationError +if salary > 50000: +print("High earner") +``` + +### Nested If Statements + +You can place `if` statements inside other `if` statements. + +```python +department = "Engineering" +salary = 80000 + +if department == "Engineering": + if salary > 75000: + print("Senior Engineer") + else: + print("Engineer") +``` + +> **Tip:** Deeply nested code is hard to read. If you find yourself going more than two levels deep, consider restructuring using `and` / `or`. + +### Truthy and Falsy Values + +In Python, values other than `True` and `False` can be treated as booleans in an `if` condition. + +| Falsy | Truthy | +|-------|--------| +| `0` | Any non-zero number | +| `""` (empty string) | Any non-empty string | +| `[]` (empty list) | Any non-empty list | +| `None` | Any object | + +```python +name = "" + +if name: + print(f"Hello, {name}") +else: + print("No name provided") +``` + +### Practice Exercises + +* Write an `if/elif/else` statement that categorises a product price: under `10` is `"Cheap"`, between `10` and `50` is `"Moderate"`, above `50` is `"Expensive"`. +* Ask the user for a number. Print whether it is positive, negative, or zero. +* Check if a `username` variable is an empty string and print an appropriate message. +* Write an `if` statement using `and` to check if a salary is between `40000` and `90000`. diff --git a/python_101/Grade 2/03 For loops.md b/python_101/Grade 2/03 For loops.md new file mode 100644 index 0000000..019b237 --- /dev/null +++ b/python_101/Grade 2/03 For loops.md @@ -0,0 +1,134 @@ +# For Loops + +### Introduction +A `for` loop iterates over a sequence — a list, string, range, or any iterable — and runs the same block of code for each item. + +### Basic Syntax + +```python +for variable in sequence: + # code to run for each item +``` + +### Iterating Over a List + +```python +employees = ["Alice", "Bob", "Charlie"] + +for employee in employees: + print(employee) +``` + +**Output** + +``` +Alice +Bob +Charlie +``` + +### Using range() + +`range()` generates a sequence of numbers. It is commonly used when you need to repeat something a set number of times. + +```python +# range(stop) — 0 up to (but not including) stop +for i in range(5): + print(i) +# 0, 1, 2, 3, 4 + +# range(start, stop) +for i in range(2, 6): + print(i) +# 2, 3, 4, 5 + +# range(start, stop, step) +for i in range(0, 10, 2): + print(i) +# 0, 2, 4, 6, 8 +``` + +### Iterating Over a String + +Strings are sequences of characters — you can loop over them directly. + +```python +for char in "Python": + print(char) +``` + +**Output** + +``` +P +y +t +h +o +n +``` + +### break and continue + +`break` exits the loop immediately. `continue` skips the rest of the current iteration and moves to the next. + +```python +salaries = [30000, 55000, 80000, 120000, 45000] + +for salary in salaries: + if salary > 100000: + print("Found a high earner, stopping") + break + print(salary) +``` + +```python +for i in range(10): + if i % 2 == 0: + continue # skip even numbers + print(i) +# 1, 3, 5, 7, 9 +``` + +### enumerate() + +When you need both the index and the value, use `enumerate()`. + +```python +employees = ["Alice", "Bob", "Charlie"] + +for index, name in enumerate(employees): + print(f"{index + 1}. {name}") +``` + +**Output** + +``` +1. Alice +2. Bob +3. Charlie +``` + +### List Comprehensions + +A compact way to create a list using a `for` loop in a single line. + +```python +# Standard loop +squares = [] +for n in range(1, 6): + squares.append(n ** 2) + +# List comprehension — same result +squares = [n ** 2 for n in range(1, 6)] + +print(squares) # [1, 4, 9, 16, 25] +``` + +### Practice Exercises + +* Loop over a list of five product names and print each one. +* Use `range()` to print the numbers 1 to 10. +* Loop over the same range but only print even numbers using `continue`. +* Create a list of salaries. Use a loop to print only those above `50000`. +* Use a list comprehension to create a list of the squares of numbers 1 to 8. diff --git a/python_101/Grade 2/04 While loops.md b/python_101/Grade 2/04 While loops.md new file mode 100644 index 0000000..92e66a7 --- /dev/null +++ b/python_101/Grade 2/04 While loops.md @@ -0,0 +1,83 @@ +# While Loops + +### Introduction +A `while` loop runs a block of code repeatedly as long as a condition remains `True`. Unlike a `for` loop, it does not iterate over a fixed sequence — it keeps going until you tell it to stop. + +### Basic Syntax + +```python +while condition: + # code to run while condition is True +``` + +**Example** + +```python +count = 0 + +while count < 5: + print(count) + count += 1 +``` + +**Output** + +``` +0 +1 +2 +3 +4 +``` + +> Always make sure something inside the loop will eventually make the condition `False`, or the loop will run forever. + +### Infinite Loops + +An infinite loop runs indefinitely. Useful in some contexts (like waiting for user input), but usually a bug. + +```python +# Intentional: keep asking until a valid response +while True: + answer = input("Type 'yes' to continue: ") + if answer == "yes": + break + print("Please type 'yes'") +``` + +Use `Ctrl+C` in the terminal to kill an unintended infinite loop. + +### break and continue + +Same as in `for` loops — `break` exits immediately, `continue` skips to the next iteration. + +```python +attempts = 0 + +while attempts < 5: + password = input("Enter password: ") + if password == "secret": + print("Access granted") + break + attempts += 1 + print(f"Wrong. {5 - attempts} attempts remaining.") +else: + print("Too many failed attempts.") +``` + +> The `else` block on a `while` loop runs only if the loop finished without hitting a `break`. + +### When to Use while vs for + +| Use `for` when | Use `while` when | +|---------------|-----------------| +| You know how many times to iterate | You don't know how many iterations are needed | +| Iterating over a list or range | Waiting for a condition to change | +| Reading items from a collection | Retry logic, polling, or user input | + +### Practice Exercises + +* Write a `while` loop that prints numbers from `10` down to `1`. +* Write a loop that keeps asking the user to guess a number until they get it right (use a fixed number as the answer). +* Use a `while` loop to sum all integers from `1` to `100` and print the result. +* Write a loop that counts how many times you can halve `1000` before it goes below `1`. diff --git a/python_101/Grade 3/01 Lists.md b/python_101/Grade 3/01 Lists.md new file mode 100644 index 0000000..06f4644 --- /dev/null +++ b/python_101/Grade 3/01 Lists.md @@ -0,0 +1,97 @@ +# Lists + +### Introduction +A list is an ordered, mutable collection of items. Items can be of any type — and a single list can mix types, though in practice you usually store similar items together. + +### Creating a List + +```python +employees = ["Alice", "Bob", "Charlie"] +salaries = [50000, 65000, 80000] +mixed = ["Alice", 30, True, None] +empty = [] +``` + +### Accessing Items + +Lists are zero-indexed — the first item is at position `0`. + +```python +employees = ["Alice", "Bob", "Charlie"] + +print(employees[0]) # Alice +print(employees[-1]) # Charlie (negative index counts from the end) +``` + +### Slicing + +Extract a portion of a list using `[start:stop]`. The `stop` index is not included. + +```python +employees = ["Alice", "Bob", "Charlie", "Diana", "Eve"] + +print(employees[1:3]) # ['Bob', 'Charlie'] +print(employees[:2]) # ['Alice', 'Bob'] +print(employees[2:]) # ['Charlie', 'Diana', 'Eve'] +``` + +### Modifying a List + +```python +employees = ["Alice", "Bob", "Charlie"] + +employees[1] = "Barbara" # Replace an item +employees.append("Diana") # Add to the end +employees.insert(0, "Zara") # Insert at a position +employees.remove("Alice") # Remove by value +employees.pop() # Remove and return the last item +employees.pop(0) # Remove and return item at index 0 +``` + +### Useful List Methods + +```python +numbers = [5, 2, 8, 1, 9, 3] + +print(len(numbers)) # 6 — number of items +print(min(numbers)) # 1 +print(max(numbers)) # 9 +print(sum(numbers)) # 28 +print(sorted(numbers)) # [1, 2, 3, 5, 8, 9] — returns new sorted list +numbers.sort() # sorts in place +numbers.reverse() # reverses in place +print(numbers.count(5)) # how many times 5 appears +print(5 in numbers) # True — membership check +``` + +### Iterating Over a List + +```python +employees = ["Alice", "Bob", "Charlie"] + +for name in employees: + print(name) +``` + +### Nested Lists + +Lists can contain other lists. + +```python +team = [ + ["Alice", "Engineering", 80000], + ["Bob", "Finance", 65000], + ["Charlie", "HR", 55000], +] + +for member in team: + print(f"{member[0]} works in {member[1]}") +``` + +### Practice Exercises + +* Create a list of five product names. Print the first and last item. +* Append two new products to your list and print the length. +* Remove the second item from your list by value and print the result. +* Create a list of numbers and use a loop to print only those greater than `50`. +* Sort your numbers list and print the minimum, maximum, and sum. diff --git a/python_101/Grade 3/02 Tuples.md b/python_101/Grade 3/02 Tuples.md new file mode 100644 index 0000000..f7085f5 --- /dev/null +++ b/python_101/Grade 3/02 Tuples.md @@ -0,0 +1,50 @@ +# Tuples + +### Introduction +A tuple is an ordered, **immutable** collection. Once created, it cannot be changed. Tuples are used when the data should not be modified — coordinates, RGB values, database records, and return values from functions are common examples. + +### Creating a Tuple + +```python +point = (10, 20) +rgb = (255, 128, 0) +single = (42,) # Note the trailing comma — required for single-item tuples +empty = () +``` + +### Accessing Items + +Same as lists — zero-indexed. + +```python +point = (10, 20) +print(point[0]) # 10 +print(point[-1]) # 20 +``` + +### Unpacking + +Tuples are often unpacked into separate variables. + +```python +point = (10, 20) +x, y = point +print(x, y) # 10 20 + +# Works with any iterable +firstname, lastname, age = "Alice", "Smith", 30 +``` + +### Tuples vs Lists + +| | Tuple | List | +|-|-------|------| +| Mutable | ❌ | ✅ | +| Syntax | `(1, 2, 3)` | `[1, 2, 3]` | +| Use when | Data should not change | Data may change | + +### Practice Exercises + +* Create a tuple containing a product name, price, and category. Unpack it into three variables and print them. +* Try to change the first value of the tuple. Read the error message and note what it says. +* Create a function that returns two values as a tuple. Call it and unpack the result. diff --git a/python_101/Grade 3/03 Dictionaries.md b/python_101/Grade 3/03 Dictionaries.md new file mode 100644 index 0000000..8a4cf48 --- /dev/null +++ b/python_101/Grade 3/03 Dictionaries.md @@ -0,0 +1,93 @@ +# Dictionaries + +### Introduction +A dictionary stores data as **key-value pairs**. Keys must be unique and are used to look up values. Dictionaries are unordered in older Python versions, but in Python 3.7+ they preserve insertion order. + +### Creating a Dictionary + +```python +employee = { + "firstname": "Alice", + "lastname": "Smith", + "department": "Engineering", + "salary": 80000 +} +``` + +### Accessing Values + +```python +print(employee["firstname"]) # Alice +print(employee.get("salary")) # 80000 +print(employee.get("age", "unknown")) # unknown — default if key missing +``` + +> Use `.get()` when the key may not exist — accessing a missing key with `[]` raises a `KeyError`. + +### Adding and Updating + +```python +employee["age"] = 30 # add a new key +employee["salary"] = 85000 # update an existing key +``` + +### Removing Items + +```python +employee.pop("age") # remove by key, returns the value +del employee["lastname"] # remove by key +``` + +### Checking Keys + +```python +if "department" in employee: + print(employee["department"]) +``` + +### Iterating Over a Dictionary + +```python +employee = {"firstname": "Alice", "department": "Engineering", "salary": 80000} + +for key in employee: + print(key) + +for key, value in employee.items(): + print(f"{key}: {value}") + +print(list(employee.keys())) # ['firstname', 'department', 'salary'] +print(list(employee.values())) # ['Alice', 'Engineering', 80000] +``` + +### Nested Dictionaries + +```python +company = { + "Alice": {"department": "Engineering", "salary": 80000}, + "Bob": {"department": "Finance", "salary": 65000}, +} + +print(company["Alice"]["salary"]) # 80000 + +for name, details in company.items(): + print(f"{name} — {details['department']}") +``` + +### Dictionary Comprehensions + +```python +salaries = {"Alice": 80000, "Bob": 65000, "Charlie": 55000} + +# Create a new dict with salaries above 60000 +high_earners = {name: salary for name, salary in salaries.items() if salary > 60000} +print(high_earners) # {'Alice': 80000, 'Bob': 65000} +``` + +### Practice Exercises + +* Create a dictionary representing a product with keys for `name`, `price`, and `category`. Print the price. +* Add a `stock` key to the dictionary. Then update the price. +* Loop over the dictionary and print each key and value on a separate line. +* Create a list of product dictionaries and loop over it to print each product name and price. +* Use `.get()` to safely access a key that does not exist, providing a default value. diff --git a/python_101/Grade 3/04 Sets.md b/python_101/Grade 3/04 Sets.md new file mode 100644 index 0000000..1aa7c9b --- /dev/null +++ b/python_101/Grade 3/04 Sets.md @@ -0,0 +1,68 @@ +# Sets + +### Introduction +A set is an **unordered collection of unique items**. Duplicates are automatically removed. Sets are useful when you care about membership and uniqueness, not order or position. + +### Creating a Set + +```python +departments = {"Engineering", "Finance", "HR", "Engineering"} +print(departments) # {'Engineering', 'Finance', 'HR'} — duplicates removed +``` + +### Adding and Removing + +```python +departments = {"Engineering", "Finance"} + +departments.add("HR") +departments.discard("Finance") # safe — no error if item missing +departments.remove("HR") # raises KeyError if item missing +``` + +### Checking Membership + +```python +print("Engineering" in departments) # True +print("Marketing" not in departments) # True +``` + +### Set Operations + +Sets support the same operations as in mathematics. + +```python +team_a = {"Alice", "Bob", "Charlie"} +team_b = {"Bob", "Diana", "Eve"} + +print(team_a | team_b) # Union — all unique items from both +print(team_a & team_b) # Intersection — only items in both +print(team_a - team_b) # Difference — items in A but not B +print(team_a ^ team_b) # Symmetric difference — items in either but not both +``` + +**Output** + +``` +{'Alice', 'Bob', 'Charlie', 'Diana', 'Eve'} +{'Bob'} +{'Alice', 'Charlie'} +{'Alice', 'Charlie', 'Diana', 'Eve'} +``` + +### Removing Duplicates from a List + +A common use case — convert a list to a set and back. + +```python +categories = ["Electronics", "Books", "Electronics", "Clothing", "Books"] +unique_categories = list(set(categories)) +print(unique_categories) # order is not guaranteed +``` + +### Practice Exercises + +* Create a set of five skills. Add two more. Discard one and print the result. +* Given two sets of employee names from different offices, find those who work in both. +* Take a list with duplicate product IDs and use a set to produce a deduplicated version. +* Check whether `"python"` is in the set `{"sql", "python", "excel"}`. diff --git a/python_101/Grade 4/01 Defining functions.md b/python_101/Grade 4/01 Defining functions.md new file mode 100644 index 0000000..ef46e1a --- /dev/null +++ b/python_101/Grade 4/01 Defining functions.md @@ -0,0 +1,49 @@ +# Defining Functions + +### Introduction +A function is a reusable block of code that performs a specific task. Instead of writing the same logic in multiple places, you define it once and call it whenever you need it. + +### Basic Syntax + +```python +def function_name(): + # code to run when the function is called +``` + +**Example** + +```python +def greet(): + print("Hello, World!") + +greet() # Hello, World! +greet() # Hello, World! +``` + +### Naming Conventions + +Function names follow the same rules as variables — lowercase with underscores (snake_case). + +```python +def calculate_salary(): + ... + +def get_employee_name(): + ... +``` + +### Docstrings + +A docstring is a string at the top of a function that describes what it does. It is good practice to include one. + +```python +def greet(): + """Print a greeting message to the terminal.""" + print("Hello, World!") +``` + +### Practice Exercises + +* Define a function called `print_header` that prints a decorative line of `=` characters. +* Call it three times and verify it prints the same output each time. +* Add a docstring to your function describing what it does. diff --git a/python_101/Grade 4/02 Parameters and return values.md b/python_101/Grade 4/02 Parameters and return values.md new file mode 100644 index 0000000..9d35d2a --- /dev/null +++ b/python_101/Grade 4/02 Parameters and return values.md @@ -0,0 +1,101 @@ +# Parameters and Return Values + +### Introduction +Parameters allow you to pass data into a function. Return values allow a function to send data back to the caller. Together they make functions truly reusable. + +### Parameters + +```python +def greet(name): + print(f"Hello, {name}!") + +greet("Alice") # Hello, Alice! +greet("Bob") # Hello, Bob! +``` + +### Multiple Parameters + +```python +def describe_employee(firstname, department, salary): + print(f"{firstname} works in {department} and earns £{salary:,}") + +describe_employee("Alice", "Engineering", 80000) +``` + +### Default Parameter Values + +If a caller does not provide a value, the default is used. + +```python +def greet(name, greeting="Hello"): + print(f"{greeting}, {name}!") + +greet("Alice") # Hello, Alice! +greet("Bob", "Good morning") # Good morning, Bob! +``` + +> Default parameters must come after non-default ones. + +### Keyword Arguments + +You can pass arguments by name, in any order. + +```python +def describe_employee(firstname, department, salary): + print(f"{firstname} — {department} — £{salary:,}") + +describe_employee(salary=80000, firstname="Alice", department="Engineering") +``` + +### Return Values + +The `return` statement exits the function and sends a value back to the caller. + +```python +def add(a, b): + return a + b + +result = add(10, 5) +print(result) # 15 +``` + +Without a `return`, a function returns `None`. + +### Returning Multiple Values + +Python functions can return multiple values as a tuple. + +```python +def get_name_and_salary(employee_id): + # In a real program, this might query a database + return "Alice", 80000 + +name, salary = get_name_and_salary(1) +print(name, salary) # Alice 80000 +``` + +### *args and **kwargs + +For functions that accept a variable number of arguments. + +```python +# *args collects extra positional arguments as a tuple +def total(*amounts): + return sum(amounts) + +print(total(100, 200, 300)) # 600 + +# **kwargs collects extra keyword arguments as a dictionary +def display(**details): + for key, value in details.items(): + print(f"{key}: {value}") + +display(name="Alice", department="Engineering", salary=80000) +``` + +### Practice Exercises + +* Write a function `calculate_tax(salary, rate=0.2)` that returns the tax amount. Test with and without providing the rate. +* Write a function `full_name(firstname, lastname)` that returns the full name as a single string. +* Write a function that takes any number of salaries using `*args` and returns the average. +* Write a function `employee_summary` that accepts keyword arguments and prints each key-value pair. diff --git a/python_101/Grade 4/03 Scope.md b/python_101/Grade 4/03 Scope.md new file mode 100644 index 0000000..d461dbf --- /dev/null +++ b/python_101/Grade 4/03 Scope.md @@ -0,0 +1,81 @@ +# Scope + +### Introduction +Scope determines where a variable can be accessed. Python uses the **LEGB** rule to look up variables: Local → Enclosing → Global → Built-in. + +### Local Scope + +Variables created inside a function exist only within that function. + +```python +def calculate(): + result = 100 # local variable + print(result) + +calculate() +print(result) # NameError — result does not exist outside the function +``` + +### Global Scope + +Variables created outside any function are global and accessible everywhere. + +```python +tax_rate = 0.2 # global variable + +def calculate_tax(salary): + return salary * tax_rate # can read the global variable + +print(calculate_tax(80000)) # 16000.0 +``` + +### Modifying a Global Variable + +Reading a global variable from inside a function works automatically. Modifying one requires the `global` keyword. + +```python +count = 0 + +def increment(): + global count + count += 1 + +increment() +increment() +print(count) # 2 +``` + +> **Tip:** Relying heavily on `global` makes code harder to follow and test. Prefer passing values in as parameters and returning updated values. + +### Enclosing Scope (Closures) + +When a function is defined inside another function, the inner function can access the outer function's variables. + +```python +def make_multiplier(factor): + def multiply(number): + return number * factor # factor comes from the enclosing scope + return multiply + +double = make_multiplier(2) +triple = make_multiplier(3) + +print(double(10)) # 20 +print(triple(10)) # 30 +``` + +### Built-in Scope + +Names like `print`, `len`, `range`, and `int` are always available — they live in Python's built-in scope. Avoid naming your own variables after them. + +```python +# Don't do this +list = [1, 2, 3] # shadows the built-in list() function +print(list([4, 5])) # TypeError — you've overwritten list +``` + +### Practice Exercises + +* Create a global variable `company_name`. Write a function that reads and prints it without using `global`. +* Write a function that tries to modify a global counter without the `global` keyword. Observe the error, then fix it. +* Write a `make_greeting(greeting)` function that returns an inner function. The inner function should accept a name and print `"{greeting}, {name}!"`. diff --git a/python_101/Grade 4/04 Lambda functions.md b/python_101/Grade 4/04 Lambda functions.md new file mode 100644 index 0000000..a440217 --- /dev/null +++ b/python_101/Grade 4/04 Lambda functions.md @@ -0,0 +1,89 @@ +# Lambda Functions + +### Introduction +A lambda function is a small, anonymous function written in a single expression. They are useful when you need a short function for a brief purpose — most often as an argument to another function. + +### Syntax + +```python +lambda parameters: expression +``` + +The expression is evaluated and returned automatically — no `return` keyword needed. + +### Basic Example + +```python +# Standard function +def square(n): + return n ** 2 + +# Equivalent lambda +square = lambda n: n ** 2 + +print(square(5)) # 25 +``` + +### Where Lambdas Are Most Useful + +Lambdas shine when passed directly to functions like `sorted()`, `map()`, and `filter()`. + +#### sorted() with a key + +```python +employees = [ + {"name": "Charlie", "salary": 55000}, + {"name": "Alice", "salary": 80000}, + {"name": "Bob", "salary": 65000}, +] + +# Sort by salary +by_salary = sorted(employees, key=lambda e: e["salary"]) +for emp in by_salary: + print(emp["name"], emp["salary"]) +``` + +**Output** +``` +Charlie 55000 +Bob 65000 +Alice 80000 +``` + +#### map() + +Apply a function to every item in an iterable. + +```python +salaries = [50000, 65000, 80000] +after_tax = list(map(lambda s: s * 0.8, salaries)) +print(after_tax) # [40000.0, 52000.0, 64000.0] +``` + +#### filter() + +Keep only the items where the function returns `True`. + +```python +salaries = [30000, 55000, 80000, 45000, 90000] +high = list(filter(lambda s: s > 60000, salaries)) +print(high) # [80000, 90000] +``` + +### Lambda vs def + +| | `def` | `lambda` | +|-|-------|---------| +| Lines | Multiple | One | +| Name | Required | Optional | +| Docstring | ✅ | ❌ | +| Best for | Complex reusable logic | Short one-off functions | + +> If a lambda grows beyond a simple expression, replace it with a proper `def`. Readability matters more than brevity. + +### Practice Exercises + +* Use `sorted()` with a lambda to sort a list of product dictionaries by price, descending. +* Use `map()` with a lambda to convert a list of prices from GBP to USD (multiply by `1.27`). +* Use `filter()` with a lambda to keep only employees whose department is `"Engineering"`. +* Rewrite the `filter()` example above using a list comprehension. Which do you prefer? diff --git a/python_101/Grade 5/01 Reading and writing files.md b/python_101/Grade 5/01 Reading and writing files.md new file mode 100644 index 0000000..06082d4 --- /dev/null +++ b/python_101/Grade 5/01 Reading and writing files.md @@ -0,0 +1,95 @@ +# Reading and Writing Files + +### Introduction +Working with files is a core skill for data roles. Python's built-in `open()` function handles reading from and writing to text files. + +### Opening a File + +```python +open(filepath, mode) +``` + +Common modes: + +| Mode | Meaning | +|------|---------| +| `"r"` | Read (default) — file must exist | +| `"w"` | Write — creates or overwrites the file | +| `"a"` | Append — adds to the end without overwriting | +| `"r+"` | Read and write | + +### The with Statement + +Always use `with` when working with files. It automatically closes the file when the block exits, even if an error occurs. + +```python +with open("employees.txt", "r") as f: + content = f.read() + +print(content) +``` + +### Reading Files + +```python +# Read the entire file as a single string +with open("employees.txt", "r") as f: + content = f.read() + +# Read one line at a time +with open("employees.txt", "r") as f: + for line in f: + print(line.strip()) # strip() removes the trailing newline + +# Read all lines into a list +with open("employees.txt", "r") as f: + lines = f.readlines() +``` + +### Writing Files + +```python +# Write — creates or overwrites +with open("output.txt", "w") as f: + f.write("Alice,Engineering,80000\n") + f.write("Bob,Finance,65000\n") + +# Append — adds to existing content +with open("output.txt", "a") as f: + f.write("Charlie,HR,55000\n") +``` + +### Writing Multiple Lines + +```python +employees = ["Alice", "Bob", "Charlie"] + +with open("names.txt", "w") as f: + for name in employees: + f.write(name + "\n") +``` + +### File Paths + +```python +# Relative path (relative to where you run the script from) +with open("data/employees.txt", "r") as f: + ... + +# Use pathlib for safer cross-platform paths (covered in Grade 5 modules section) +from pathlib import Path +path = Path("data") / "employees.txt" +with open(path, "r") as f: + ... +``` + +### Handling Missing Files + +Trying to open a file that does not exist in `"r"` mode raises a `FileNotFoundError`. Error handling is covered in Grade 6 — for now, make sure the file exists before reading. + +### Practice Exercises + +* Create a file called `products.txt` and write five product names to it, one per line. +* Read the file back and print each line without the trailing newline character. +* Append two more product names to the file and confirm they appear at the end. +* Read the file into a list and print only the lines that contain the letter `"a"`. diff --git a/python_101/Grade 5/02 CSV files.md b/python_101/Grade 5/02 CSV files.md new file mode 100644 index 0000000..b4a183f --- /dev/null +++ b/python_101/Grade 5/02 CSV files.md @@ -0,0 +1,100 @@ +# Working with CSV Files + +### Introduction +CSV (Comma-Separated Values) is one of the most common formats for exchanging tabular data. Python's built-in `csv` module handles the parsing so you do not have to split strings manually. + +### Reading a CSV File + +Given a file `employees.csv`: + +``` +firstname,lastname,department,salary +Alice,Smith,Engineering,80000 +Bob,Jones,Finance,65000 +Charlie,Brown,HR,55000 +``` + +```python +import csv + +with open("employees.csv", "r", newline="") as f: + reader = csv.reader(f) + for row in reader: + print(row) +``` + +**Output** +``` +['firstname', 'lastname', 'department', 'salary'] +['Alice', 'Smith', 'Engineering', '80000'] +['Bob', 'Jones', 'Finance', '65000'] +['Charlie', 'Brown', 'HR', '55000'] +``` + +> Always pass `newline=""` when opening CSV files — this prevents `csv.reader` from mishandling line endings on Windows. + +### Skipping the Header Row + +```python +import csv + +with open("employees.csv", "r", newline="") as f: + reader = csv.reader(f) + next(reader) # skip the header row + for row in reader: + print(f"{row[0]} {row[1]} — {row[3]}") +``` + +### DictReader: Reading Into Dictionaries + +`DictReader` maps each row to a dictionary using the header row as keys. This is usually more readable than using column indexes. + +```python +import csv + +with open("employees.csv", "r", newline="") as f: + reader = csv.DictReader(f) + for row in reader: + print(f"{row['firstname']} works in {row['department']} earning £{row['salary']}") +``` + +### Writing a CSV File + +```python +import csv + +employees = [ + ["Alice", "Smith", "Engineering", 80000], + ["Bob", "Jones", "Finance", 65000], +] + +with open("output.csv", "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["firstname", "lastname", "department", "salary"]) # header + writer.writerows(employees) +``` + +### DictWriter: Writing From Dictionaries + +```python +import csv + +employees = [ + {"firstname": "Alice", "department": "Engineering", "salary": 80000}, + {"firstname": "Bob", "department": "Finance", "salary": 65000}, +] + +fields = ["firstname", "department", "salary"] + +with open("output.csv", "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fields) + writer.writeheader() + writer.writerows(employees) +``` + +### Practice Exercises + +* Create a CSV file of five products with columns `name`, `category`, and `price`. Write it using `csv.writer`. +* Read the file back using `DictReader` and print only the products with a price above `20`. +* Add a new column `discounted_price` (price * 0.9) and write the updated data to a new CSV file. +* Read a CSV, collect all unique categories into a set, and print them. diff --git a/python_101/Grade 5/03 Importing modules.md b/python_101/Grade 5/03 Importing modules.md new file mode 100644 index 0000000..ee03974 --- /dev/null +++ b/python_101/Grade 5/03 Importing modules.md @@ -0,0 +1,112 @@ +# Importing Modules + +### Introduction +A module is a Python file containing functions, classes, and variables that you can reuse across your programs. Python comes with a large standard library of built-in modules — no installation required. + +### Importing a Module + +```python +import math + +print(math.pi) # 3.141592653589793 +print(math.sqrt(144)) # 12.0 +print(math.ceil(4.2)) # 5 +print(math.floor(4.9)) # 4 +``` + +### Importing Specific Names + +```python +from math import sqrt, pi + +print(sqrt(144)) # 12.0 +print(pi) # 3.141592653589793 +``` + +### Aliases + +```python +import math as m +from datetime import datetime as dt + +print(m.sqrt(25)) +print(dt.now()) +``` + +### Useful Standard Library Modules + +#### datetime — Dates and Times + +```python +from datetime import datetime, date, timedelta + +today = date.today() +print(today) # 2024-11-01 + +now = datetime.now() +print(now.strftime("%d/%m/%Y %H:%M")) # 01/11/2024 14:30 + +in_30_days = today + timedelta(days=30) +print(in_30_days) +``` + +#### os — Operating System Interaction + +```python +import os + +print(os.getcwd()) # current working directory +print(os.listdir(".")) # files in current directory +os.makedirs("data", exist_ok=True) # create a directory +``` + +#### pathlib — File Paths (Modern Approach) + +```python +from pathlib import Path + +p = Path("data") / "employees.csv" +print(p.exists()) # True or False +print(p.suffix) # .csv +print(p.stem) # employees +print(p.parent) # data +``` + +#### random — Random Numbers + +```python +import random + +print(random.randint(1, 100)) # random integer 1–100 +print(random.choice(["Alice", "Bob"])) # random item from list +random.shuffle([1, 2, 3, 4, 5]) # shuffle in place +``` + +#### json — JSON Data + +```python +import json + +data = {"name": "Alice", "department": "Engineering", "salary": 80000} + +json_string = json.dumps(data, indent=2) # dict → JSON string +print(json_string) + +parsed = json.loads(json_string) # JSON string → dict +print(parsed["name"]) + +# Write to file +with open("employee.json", "w") as f: + json.dump(data, f, indent=2) + +# Read from file +with open("employee.json", "r") as f: + loaded = json.load(f) +``` + +### Practice Exercises + +* Use the `datetime` module to print today's date in the format `DD-MM-YYYY`. +* Use `os.listdir()` to list all files in the current directory. +* Use `random.choice()` to pick a random item from a list of five department names. +* Read the CSV you created in the previous lesson and write its contents to a JSON file using the `json` module. diff --git a/python_101/Grade 5/04 Creating modules.md b/python_101/Grade 5/04 Creating modules.md new file mode 100644 index 0000000..ebc0d66 --- /dev/null +++ b/python_101/Grade 5/04 Creating modules.md @@ -0,0 +1,95 @@ +# Creating Your Own Modules + +### Introduction +Any `.py` file is a module. Splitting your code across multiple files keeps things organised and makes logic reusable across different scripts. + +### Creating a Module + +Create a file called `utils.py`: + +```python +# utils.py + +def calculate_tax(salary, rate=0.2): + """Return the tax amount for a given salary.""" + return salary * rate + +def full_name(firstname, lastname): + """Return a formatted full name.""" + return f"{firstname} {lastname}" + +TAX_RATE = 0.2 +``` + +### Importing Your Module + +In another file in the same directory: + +```python +# main.py +import utils + +name = utils.full_name("Alice", "Smith") +tax = utils.calculate_tax(80000) + +print(f"{name} owes £{tax:,.2f} in tax") +``` + +Or import specific names: + +```python +from utils import full_name, calculate_tax + +print(full_name("Alice", "Smith")) +print(calculate_tax(80000, rate=0.3)) +``` + +### The if __name__ == "__main__" Guard + +When Python runs a file directly, it sets `__name__` to `"__main__"`. When the file is imported as a module, `__name__` is set to the module name instead. + +This guard lets you include test code in a module that only runs when the file is executed directly — not when it is imported. + +```python +# utils.py + +def calculate_tax(salary, rate=0.2): + return salary * rate + +if __name__ == "__main__": + # This block only runs when utils.py is executed directly + print(calculate_tax(80000)) +``` + +```bash +python utils.py # prints 16000.0 +python main.py # does NOT print 16000.0 +``` + +### Organising Modules into Packages + +A **package** is a directory containing an `__init__.py` file. This signals to Python that the directory should be treated as a package. + +``` +project/ +├── main.py +└── payroll/ + ├── __init__.py + ├── tax.py + └── employees.py +``` + +```python +# main.py +from payroll.tax import calculate_tax +from payroll.employees import full_name +``` + +The `__init__.py` can be empty, or it can import names to make them available at the package level. + +### Practice Exercises + +* Create a `helpers.py` module with two functions: one that formats a salary as a string (e.g. `"£80,000"`) and one that checks whether a salary is above a threshold. +* Import and use both functions in a separate `main.py` file. +* Add the `if __name__ == "__main__"` guard to `helpers.py` with a quick test of both functions. +* Create a `data/` package with an `__init__.py` and a `loader.py` file. In `loader.py` write a function that reads a CSV file and returns a list of dictionaries. diff --git a/python_101/Grade 6/01 Errors and exceptions.md b/python_101/Grade 6/01 Errors and exceptions.md new file mode 100644 index 0000000..26377ec --- /dev/null +++ b/python_101/Grade 6/01 Errors and exceptions.md @@ -0,0 +1,61 @@ +# Errors and Exceptions + +### Introduction +Errors are a normal part of programming. Understanding the different types and how to read a traceback is the first step to fixing them quickly. + +### Types of Errors + +#### Syntax Errors +The code is not valid Python. The interpreter catches this before running anything. + +```python +if salary > 50000 + print("High earner") +# SyntaxError: expected ':' +``` + +#### Runtime Errors (Exceptions) +The code is valid Python but something goes wrong when it runs. + +```python +salary = int("not a number") # ValueError +employee = {} +print(employee["name"]) # KeyError +result = 10 / 0 # ZeroDivisionError +``` + +### Common Built-in Exceptions + +| Exception | Caused by | +|-----------|-----------| +| `ValueError` | Wrong value type, e.g. `int("hello")` | +| `TypeError` | Wrong type, e.g. `"age: " + 30` | +| `KeyError` | Missing dictionary key | +| `IndexError` | List index out of range | +| `FileNotFoundError` | File does not exist | +| `ZeroDivisionError` | Dividing by zero | +| `AttributeError` | Calling a method that does not exist | +| `NameError` | Variable not defined | +| `ImportError` | Module not found | + +### Reading a Traceback + +When an unhandled exception occurs, Python prints a traceback. Read it **bottom to top** — the last line is the error, the lines above show where it happened. + +``` +Traceback (most recent call last): + File "main.py", line 8, in + result = calculate(value) + File "main.py", line 3, in calculate + return 10 / value +ZeroDivisionError: division by zero +``` + +The error is `ZeroDivisionError`, it occurred in the `calculate` function on line 3, called from line 8 in the main script. + +### Practice Exercises + +* Write code that deliberately causes a `TypeError`. Read the traceback and identify the line number. +* Cause a `KeyError` by accessing a key that does not exist in a dictionary. Note the key name in the error. +* Cause an `IndexError` by accessing an index that is out of range. +* Write a short function and call it with the wrong number of arguments. Identify the exception type. diff --git a/python_101/Grade 6/02 Try and except.md b/python_101/Grade 6/02 Try and except.md new file mode 100644 index 0000000..77879b7 --- /dev/null +++ b/python_101/Grade 6/02 Try and except.md @@ -0,0 +1,107 @@ +# Try and Except + +### Introduction +The `try/except` block lets you catch exceptions and handle them gracefully instead of letting the program crash. + +### Basic Syntax + +```python +try: + # code that might raise an exception +except ExceptionType: + # code to run if that exception occurs +``` + +**Example** + +```python +try: + age = int(input("Enter your age: ")) + print(f"In 10 years you will be {age + 10}") +except ValueError: + print("Please enter a valid number.") +``` + +### Catching Multiple Exceptions + +```python +try: + value = int(input("Enter a number: ")) + result = 100 / value + print(result) +except ValueError: + print("That is not a valid number.") +except ZeroDivisionError: + print("Cannot divide by zero.") +``` + +### Catching Multiple in One Except + +```python +try: + ... +except (ValueError, TypeError): + print("Invalid input.") +``` + +### The else Clause + +The `else` block runs only if no exception was raised. + +```python +try: + value = int(input("Enter a number: ")) +except ValueError: + print("Not a number.") +else: + print(f"You entered {value}") # only runs if no exception +``` + +### The finally Clause + +`finally` always runs, whether or not an exception occurred. Use it for cleanup — closing a file, releasing a resource. + +```python +try: + f = open("data.csv", "r") + content = f.read() +except FileNotFoundError: + print("File not found.") +finally: + print("Done.") # always runs +``` + +> In practice, the `with` statement handles file cleanup automatically — you rarely need `finally` for files. + +### Accessing the Exception + +Use `as` to capture the exception object and read its message. + +```python +try: + result = int("not a number") +except ValueError as e: + print(f"Error: {e}") # Error: invalid literal for int() with base 10: 'not a number' +``` + +### A Practical Pattern: Validating User Input + +```python +def get_integer(prompt): + """Keep asking until the user enters a valid integer.""" + while True: + try: + return int(input(prompt)) + except ValueError: + print("Invalid input. Please enter a whole number.") + +age = get_integer("Enter your age: ") +print(f"Age: {age}") +``` + +### Practice Exercises + +* Write a function `safe_divide(a, b)` that returns `a / b` or prints a message and returns `None` if `b` is zero. +* Write a function `read_file(path)` that returns the file contents or a clear error message if the file does not exist. +* Use a `try/except/else/finally` block — print a different message in each clause and observe the output both when an error occurs and when it does not. +* Write a loop that keeps asking for a valid positive integer, rejecting anything that is not a number or is less than 1. diff --git a/python_101/Grade 6/03 Raising exceptions.md b/python_101/Grade 6/03 Raising exceptions.md new file mode 100644 index 0000000..68a6e1b --- /dev/null +++ b/python_101/Grade 6/03 Raising exceptions.md @@ -0,0 +1,61 @@ +# Raising Exceptions + +### Introduction +You can raise your own exceptions to signal that something has gone wrong in your code. This makes your functions self-documenting and prevents bad data from propagating silently. + +### raise + +```python +def set_salary(salary): + if salary < 0: + raise ValueError("Salary cannot be negative.") + return salary + +set_salary(-5000) +# ValueError: Salary cannot be negative. +``` + +### Raising with a Custom Message + +```python +def get_employee(employee_id, employees): + if employee_id not in employees: + raise KeyError(f"No employee found with ID {employee_id}") + return employees[employee_id] +``` + +### Custom Exception Classes + +For larger projects, define your own exception types by subclassing `Exception`. + +```python +class InvalidSalaryError(Exception): + """Raised when a salary value is invalid.""" + pass + +class EmployeeNotFoundError(Exception): + """Raised when an employee cannot be found.""" + pass + +def set_salary(salary): + if not isinstance(salary, (int, float)): + raise TypeError("Salary must be a number.") + if salary < 0: + raise InvalidSalaryError(f"Salary {salary} cannot be negative.") + return salary +``` + +Callers can then catch your specific exception: + +```python +try: + set_salary(-5000) +except InvalidSalaryError as e: + print(f"Payroll error: {e}") +``` + +### Practice Exercises + +* Write a function `set_age(age)` that raises a `ValueError` if age is below 0 or above 150. +* Create a custom exception class `InsufficientStockError`. Write a function that raises it when stock goes below zero. +* Write a function that raises an exception and then call it inside a `try/except` that catches and prints the message. diff --git a/python_101/Grade 6/04 Debugging.md b/python_101/Grade 6/04 Debugging.md new file mode 100644 index 0000000..0fded5f --- /dev/null +++ b/python_101/Grade 6/04 Debugging.md @@ -0,0 +1,90 @@ +# Debugging Techniques + +### Introduction +Debugging is the process of finding and fixing the cause of unexpected behaviour. The goal is not just to fix the symptom but to understand why it happened. + +### 1. Read the Traceback + +The traceback is your first source of information. Read it bottom to top — the last line names the exception, the lines above show the call chain that led to it. + +``` +Traceback (most recent call last): + File "main.py", line 12, in + process(data) + File "main.py", line 7, in process + total = sum(values) +TypeError: unsupported operand type(s) for +: 'int' and 'str' +``` + +Here the problem is a string in a list that should contain only integers, on line 7 of `process()`. + +### 2. Print Debugging + +The simplest technique — add `print()` calls to inspect values at different points. + +```python +def calculate_bonus(salary, multiplier): + print(f"DEBUG salary={salary}, multiplier={multiplier}") + bonus = salary * multiplier + print(f"DEBUG bonus={bonus}") + return bonus +``` + +Remove or comment out debug prints before sharing your code. + +### 3. The Python Debugger (pdb) + +`pdb` lets you pause execution and step through code line by line. + +```python +import pdb + +def calculate_bonus(salary, multiplier): + pdb.set_trace() # execution pauses here + return salary * multiplier +``` + +Useful `pdb` commands at the prompt: + +| Command | Action | +|---------|--------| +| `n` | Next line | +| `s` | Step into a function | +| `c` | Continue to next breakpoint | +| `p variable` | Print a variable's value | +| `q` | Quit the debugger | + +In Python 3.7+ you can use `breakpoint()` instead of `import pdb; pdb.set_trace()`. + +```python +def calculate_bonus(salary, multiplier): + breakpoint() + return salary * multiplier +``` + +### 4. VS Code Debugger + +VS Code has a built-in debugger with a visual interface: + +1. Click the line number in the editor to set a **breakpoint** (a red dot appears). +2. Press **F5** or click **Run → Start Debugging**. +3. Execution pauses at the breakpoint. +4. Use the debug toolbar to step through, inspect variables, and continue. + +This is the most productive approach for anything beyond a quick print check. + +### 5. Common Bugs and How to Find Them + +| Symptom | Likely cause | Where to look | +|---------|-------------|---------------| +| `TypeError` on arithmetic | A string where a number is expected | Check `type()` of inputs | +| `KeyError` on dict access | Key does not exist | Use `.get()` or check with `in` | +| Wrong output from a loop | Off-by-one error | Print the loop variable on each iteration | +| Function returns `None` | Missing `return` | Check every code path has a `return` | +| Variable has unexpected value | Modified elsewhere | Search for all assignments to that name | + +### Practice Exercises + +* Take a function with a deliberate bug (wrong operator, missing return) and use print debugging to find and fix it. +* Set a `breakpoint()` in a function and step through it, printing the value of a variable at each step. +* Write a function that receives a list of mixed types (integers and strings) and raises a `TypeError` when it hits a string. Use the traceback to identify exactly which value caused the problem. diff --git a/python_101/Grade 7/01 Classes and objects.md b/python_101/Grade 7/01 Classes and objects.md new file mode 100644 index 0000000..363a186 --- /dev/null +++ b/python_101/Grade 7/01 Classes and objects.md @@ -0,0 +1,57 @@ +# Classes and Objects + +### Introduction +Object-Oriented Programming (OOP) organises code around **objects** — bundles of related data and behaviour. A **class** is the blueprint; an **object** (or instance) is what you create from it. + +### Defining a Class + +```python +class Employee: + pass # empty class for now +``` + +### Creating an Instance + +```python +emp = Employee() +print(type(emp)) # +``` + +### The __init__ Method + +`__init__` is called automatically when a new instance is created. Use it to set the initial state of the object. + +```python +class Employee: + def __init__(self, firstname, lastname, department, salary): + self.firstname = firstname + self.lastname = lastname + self.department = department + self.salary = salary + +emp = Employee("Alice", "Smith", "Engineering", 80000) +print(emp.firstname) # Alice +print(emp.salary) # 80000 +``` + +`self` refers to the instance being created. It must be the first parameter of every method, though you never pass it yourself when calling. + +### Accessing Attributes + +```python +emp = Employee("Alice", "Smith", "Engineering", 80000) + +# Read +print(emp.department) + +# Update +emp.salary = 85000 +print(emp.salary) +``` + +### Practice Exercises + +* Define a `Product` class with attributes `name`, `category`, and `price`. +* Create three `Product` instances with different values and print all their attributes. +* Update the price of one product and print it again to confirm the change. +* Create a list of five `Product` objects and use a loop to print each product's name and price. diff --git a/python_101/Grade 7/02 Attributes and methods.md b/python_101/Grade 7/02 Attributes and methods.md new file mode 100644 index 0000000..d6c2736 --- /dev/null +++ b/python_101/Grade 7/02 Attributes and methods.md @@ -0,0 +1,93 @@ +# Attributes and Methods + +### Introduction +Attributes store an object's data. Methods are functions defined on a class that describe what an object can do. + +### Instance Methods + +Methods receive `self` as the first argument, giving them access to the instance's attributes. + +```python +class Employee: + def __init__(self, firstname, lastname, salary): + self.firstname = firstname + self.lastname = lastname + self.salary = salary + + def full_name(self): + return f"{self.firstname} {self.lastname}" + + def calculate_tax(self, rate=0.2): + return self.salary * rate + + def apply_raise(self, percent): + self.salary = self.salary * (1 + percent / 100) + +emp = Employee("Alice", "Smith", 80000) +print(emp.full_name()) # Alice Smith +print(emp.calculate_tax()) # 16000.0 +emp.apply_raise(10) +print(emp.salary) # 88000.0 +``` + +### Class Attributes + +Class attributes are shared across all instances — useful for constants or defaults. + +```python +class Employee: + company = "Fynes Forge" # class attribute + default_tax_rate = 0.2 + + def __init__(self, firstname, salary): + self.firstname = firstname # instance attribute + self.salary = salary + +emp1 = Employee("Alice", 80000) +emp2 = Employee("Bob", 65000) + +print(emp1.company) # Fynes Forge +print(emp2.company) # Fynes Forge +print(Employee.company) # Fynes Forge +``` + +### Static Methods + +Static methods do not receive `self` or the class — they are utility functions that logically belong to the class but do not need access to its state. + +```python +class Employee: + @staticmethod + def is_valid_salary(salary): + return isinstance(salary, (int, float)) and salary >= 0 + +print(Employee.is_valid_salary(80000)) # True +print(Employee.is_valid_salary(-100)) # False +``` + +### Class Methods + +Class methods receive the class (`cls`) instead of the instance. They are often used as alternative constructors. + +```python +class Employee: + def __init__(self, firstname, lastname, salary): + self.firstname = firstname + self.lastname = lastname + self.salary = salary + + @classmethod + def from_dict(cls, data): + return cls(data["firstname"], data["lastname"], data["salary"]) + +data = {"firstname": "Alice", "lastname": "Smith", "salary": 80000} +emp = Employee.from_dict(data) +print(emp.full_name() if hasattr(emp, 'full_name') else emp.firstname) +``` + +### Practice Exercises + +* Add a `describe()` method to your `Product` class that prints a formatted summary of the product. +* Add a class attribute `currency = "GBP"` and use it in the `describe()` method. +* Add a `apply_discount(percent)` method that reduces the product's price. +* Add a `@staticmethod` called `is_valid_price(price)` that returns `True` if price is a positive number. diff --git a/python_101/Grade 7/03 Inheritance.md b/python_101/Grade 7/03 Inheritance.md new file mode 100644 index 0000000..2b1fc9d --- /dev/null +++ b/python_101/Grade 7/03 Inheritance.md @@ -0,0 +1,72 @@ +# Inheritance + +### Introduction +Inheritance allows a class to take on the attributes and methods of another class. The inheriting class is called the **child** (or subclass); the class it inherits from is the **parent** (or superclass). Use it to model "is-a" relationships and avoid duplicating code. + +### Basic Inheritance + +```python +class Employee: + def __init__(self, firstname, lastname, salary): + self.firstname = firstname + self.lastname = lastname + self.salary = salary + + def full_name(self): + return f"{self.firstname} {self.lastname}" + + def describe(self): + return f"{self.full_name()} — £{self.salary:,}" + + +class Manager(Employee): + def __init__(self, firstname, lastname, salary, team_size): + super().__init__(firstname, lastname, salary) # call parent __init__ + self.team_size = team_size + + def describe(self): + return f"{super().describe()} — manages {self.team_size} people" + + +emp = Employee("Alice", "Smith", 80000) +mgr = Manager("Bob", "Jones", 110000, 8) + +print(emp.describe()) # Alice Smith — £80,000 +print(mgr.describe()) # Bob Jones — £110,000 — manages 8 people +print(mgr.full_name()) # Bob Jones — inherited from Employee +``` + +### super() + +`super()` gives you access to the parent class. It is most commonly used to call the parent's `__init__` so you do not have to repeat attribute assignments. + +### Checking Inheritance + +```python +print(isinstance(mgr, Manager)) # True +print(isinstance(mgr, Employee)) # True — a Manager IS an Employee +print(issubclass(Manager, Employee)) # True +``` + +### Multiple Inheritance + +Python supports inheriting from more than one parent, though it is best used sparingly. + +```python +class Auditable: + def audit_log(self): + return f"Record created for {self.full_name()}" + +class AuditedEmployee(Employee, Auditable): + pass + +ae = AuditedEmployee("Charlie", "Brown", 70000) +print(ae.audit_log()) # Record created for Charlie Brown +``` + +### Practice Exercises + +* Create a `DigitalProduct` class that inherits from your `Product` class and adds a `download_url` attribute. +* Override the `describe()` method in `DigitalProduct` to include the URL. +* Create a `PhysicalProduct` subclass that adds a `weight_kg` attribute. +* Check using `isinstance` that a `DigitalProduct` is also an instance of `Product`. diff --git a/python_101/Grade 7/04 Dunder methods.md b/python_101/Grade 7/04 Dunder methods.md new file mode 100644 index 0000000..ec4a0d8 --- /dev/null +++ b/python_101/Grade 7/04 Dunder methods.md @@ -0,0 +1,105 @@ +# Dunder Methods + +### Introduction +Dunder methods (short for "double underscore") let you define how your objects behave with Python's built-in operations — things like printing, comparing, and using `len()`. They are also called magic methods or special methods. + +### __str__ and __repr__ + +`__str__` controls what `print()` and `str()` show. `__repr__` is the developer-facing representation used in the REPL and for debugging. + +```python +class Employee: + def __init__(self, firstname, lastname, salary): + self.firstname = firstname + self.lastname = lastname + self.salary = salary + + def __str__(self): + return f"{self.firstname} {self.lastname} (£{self.salary:,})" + + def __repr__(self): + return f"Employee('{self.firstname}', '{self.lastname}', {self.salary})" + +emp = Employee("Alice", "Smith", 80000) +print(emp) # Alice Smith (£80,000) +print(repr(emp)) # Employee('Alice', 'Smith', 80000) +``` + +### __len__ + +```python +class Department: + def __init__(self, name, employees): + self.name = name + self.employees = employees + + def __len__(self): + return len(self.employees) + +dept = Department("Engineering", ["Alice", "Bob", "Charlie"]) +print(len(dept)) # 3 +``` + +### __eq__ and __lt__ + +Define how objects are compared. + +```python +class Employee: + def __init__(self, firstname, salary): + self.firstname = firstname + self.salary = salary + + def __eq__(self, other): + return self.salary == other.salary + + def __lt__(self, other): + return self.salary < other.salary + +emp1 = Employee("Alice", 80000) +emp2 = Employee("Bob", 65000) +emp3 = Employee("Charlie", 80000) + +print(emp1 == emp3) # True +print(emp2 < emp1) # True +print(sorted([emp1, emp2, emp3], key=lambda e: e.salary)) +``` + +### __contains__ + +```python +class Department: + def __init__(self, employees): + self.employees = employees + + def __contains__(self, name): + return name in self.employees + +dept = Department(["Alice", "Bob", "Charlie"]) +print("Alice" in dept) # True +print("Diana" in dept) # False +``` + +### __getitem__ + +Makes objects subscriptable (accessible with `[]`). + +```python +class Department: + def __init__(self, employees): + self.employees = employees + + def __getitem__(self, index): + return self.employees[index] + +dept = Department(["Alice", "Bob", "Charlie"]) +print(dept[0]) # Alice +print(dept[-1]) # Charlie +``` + +### Practice Exercises + +* Add `__str__` and `__repr__` to your `Product` class. Print a product object and inspect its `repr`. +* Add `__eq__` to compare two products by price. +* Add `__lt__` and use `sorted()` on a list of product objects. +* Create a `Catalogue` class that holds a list of products and implements `__len__` and `__contains__`. diff --git a/python_101/Grade 8/01 What is pip.md b/python_101/Grade 8/01 What is pip.md new file mode 100644 index 0000000..852c1da --- /dev/null +++ b/python_101/Grade 8/01 What is pip.md @@ -0,0 +1,72 @@ +# What is pip + +### Introduction +`pip` is Python's package installer. It lets you download and install third-party libraries from the Python Package Index ([PyPI](https://pypi.org/)) — a public repository of over 500,000 packages. + +### Basic Usage + +```bash +# Install a package +pip install requests + +# Install a specific version +pip install requests==2.31.0 + +# Install the latest compatible version within a range +pip install "requests>=2.28,<3" + +# Upgrade an installed package +pip install --upgrade requests + +# Uninstall a package +pip uninstall requests + +# List installed packages +pip list + +# Show details about a specific package +pip show requests +``` + +### Searching for Packages + +Rather than searching from the command line (the search command was removed), go to [pypi.org](https://pypi.org/) and search there. Check: + +* How recently it was updated +* How many downloads it has +* Whether it has a linked GitHub repo with active maintenance + +### pip and Virtual Environments + +Always activate your virtual environment before running `pip install`. Packages installed without an active environment go into the system Python, which can cause version conflicts between projects. + +```bash +# Activate first +source .venv/bin/activate # macOS/Linux +.venv\Scripts\activate # Windows + +# Then install +pip install requests +``` + +Grade 8 covers virtual environments in detail. + +### Commonly Used Packages in Data Roles + +| Package | Use | +|---------|-----| +| `pandas` | Tabular data manipulation | +| `polars` | Fast DataFrames (modern alternative to pandas) | +| `requests` | HTTP requests and APIs | +| `sqlalchemy` | Database connections and ORM | +| `boto3` | AWS SDK | +| `pydantic` | Data validation | +| `pytest` | Testing | +| `black` | Code formatting | + +### Practice Exercises + +* Run `pip list` and note which packages are currently installed. +* Install the `requests` package and verify it appears in `pip list`. +* Run `pip show requests` and note the version, author, and homepage. +* Uninstall `requests` and confirm it no longer appears in `pip list`. diff --git a/python_101/Grade 8/02 Virtual environments.md b/python_101/Grade 8/02 Virtual environments.md new file mode 100644 index 0000000..a4f46fc --- /dev/null +++ b/python_101/Grade 8/02 Virtual environments.md @@ -0,0 +1,77 @@ +# Virtual Environments + +### Introduction +A virtual environment is an isolated Python installation for a single project. It has its own copy of `pip` and its own set of installed packages, completely separate from your system Python and other projects. + +Without virtual environments, all packages from all projects are installed globally. This leads to version conflicts — Project A needs `requests==2.28` but Project B needs `requests==2.31`, and they cannot both be satisfied at the same time. + +### Creating a Virtual Environment + +Python's built-in `venv` module handles this. + +```bash +# Create a virtual environment called .venv in the current directory +python -m venv .venv +``` + +The name `.venv` is the convention. The dot prefix hides it from directory listings by default on macOS and Linux. + +### Activating + +You must activate the environment before using it. Once active, `python` and `pip` refer to the environment's versions. + +```bash +# macOS / Linux +source .venv/bin/activate + +# Windows (Command Prompt) +.venv\Scripts\activate + +# Windows (PowerShell) +.venv\Scripts\Activate.ps1 +``` + +Your prompt will change to show `(.venv)` when the environment is active. + +### Deactivating + +```bash +deactivate +``` + +### Checking You Are in the Right Environment + +```bash +which python # macOS/Linux — should show .venv/bin/python +where python # Windows — should show .venv\Scripts\python.exe +pip list # should only show packages for this project +``` + +### Never Commit the .venv Folder + +Add `.venv` to your `.gitignore`. It is large, OS-specific, and regeneratable from your dependency file. + +``` +# .gitignore +.venv/ +``` + +### Recreating an Environment + +Because you never commit `.venv`, anyone cloning your project creates their own: + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +Dependency files are covered in the next lesson. + +### Practice Exercises + +* Create a new directory called `test-project`. Inside it, create a virtual environment. +* Activate it and run `pip list` — note that it starts nearly empty. +* Install `requests` into this environment. Confirm it is installed. +* Deactivate and run `pip list` again to confirm `requests` is not in the global environment. +* Delete the `.venv` folder and recreate it from scratch. diff --git a/python_101/Grade 8/03 uv.md b/python_101/Grade 8/03 uv.md new file mode 100644 index 0000000..34192d9 --- /dev/null +++ b/python_101/Grade 8/03 uv.md @@ -0,0 +1,95 @@ +# uv + +### Introduction +`uv` is a fast, modern Python package and project manager written in Rust. It replaces `pip`, `venv`, and in many cases `pip-tools`, running significantly faster than any of them. It is rapidly becoming the standard tool in professional Python projects. + +### Installing uv + +```bash +# macOS and Linux +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Windows (PowerShell) +powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +``` + +Verify: + +```bash +uv --version +``` + +### Creating a Project + +```bash +uv init my-project +cd my-project +``` + +This creates a project directory with a `pyproject.toml`, a `.python-version` file, and a starter `main.py`. + +### Managing Python Versions + +One of `uv`'s most useful features — it can download and manage Python versions for you. + +```bash +# Install a specific Python version +uv python install 3.12 + +# Pin the project to a specific version +uv python pin 3.12 +``` + +### Adding Packages + +```bash +# Add a package (creates/updates .venv automatically) +uv add requests +uv add pandas polars + +# Add a development-only package +uv add --dev pytest black + +# Remove a package +uv remove requests +``` + +`uv add` automatically updates `pyproject.toml` and `uv.lock`. + +### Running Scripts + +```bash +# Run a script using the project's environment +uv run main.py + +# Run a tool without installing it permanently +uv run --with black black my_script.py +``` + +### Syncing the Environment + +After cloning a project or pulling changes: + +```bash +uv sync +``` + +This installs all dependencies from `uv.lock` exactly, ensuring everyone on the team has the same environment. + +### uv vs pip + venv + +| Task | pip + venv | uv | +|------|------------|-----| +| Create environment | `python -m venv .venv` | `uv init` or `uv sync` | +| Install package | `pip install requests` | `uv add requests` | +| Install from file | `pip install -r requirements.txt` | `uv sync` | +| Speed | Baseline | 10–100× faster | +| Lock file | Manual (`pip freeze`) | Automatic (`uv.lock`) | + +### Practice Exercises + +* Install `uv` and verify the version. +* Create a new project with `uv init`. Inspect the generated `pyproject.toml`. +* Add `requests` and `pytest` to the project. Check that `pyproject.toml` reflects both. +* Run `uv run main.py` to confirm the project environment works. +* Run `uv sync` in a cloned project (or after deleting `.venv`) and confirm the environment is restored. diff --git a/python_101/Grade 8/04 Dependency files.md b/python_101/Grade 8/04 Dependency files.md new file mode 100644 index 0000000..086ec3a --- /dev/null +++ b/python_101/Grade 8/04 Dependency files.md @@ -0,0 +1,108 @@ +# requirements.txt and pyproject.toml + +### Introduction +Dependency files record which packages your project needs so anyone can recreate your environment exactly. There are two main formats — `requirements.txt` (traditional) and `pyproject.toml` (modern standard). + +### requirements.txt + +The simplest format. A plain text file listing packages, one per line. + +#### Generating One + +```bash +# Snapshot your current environment +pip freeze > requirements.txt +``` + +Example output: + +``` +certifi==2024.2.2 +charset-normalizer==3.3.2 +idna==3.7 +requests==2.31.0 +urllib3==2.2.1 +``` + +#### Installing From One + +```bash +pip install -r requirements.txt +``` + +#### Keeping It Clean + +`pip freeze` includes every package including transitive dependencies. A cleaner approach is to only list your direct dependencies manually: + +``` +# requirements.txt +requests>=2.28 +pandas>=2.0 +``` + +Then use `pip freeze` as a separate lock file if you need exact pinning. + +### pyproject.toml + +The modern, PEP-standard file for Python projects. It stores project metadata, dependencies, and tool configuration in one place. + +```toml +[project] +name = "my-project" +version = "0.1.0" +description = "A data processing project" +requires-python = ">=3.10" +dependencies = [ + "requests>=2.28", + "pandas>=2.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", + "black>=24.0", +] + +[tool.black] +line-length = 88 + +[tool.pytest.ini_options] +testpaths = ["tests"] +``` + +#### Installing From pyproject.toml + +```bash +# With pip +pip install -e . # installs the project in editable mode +pip install -e ".[dev]" # includes dev dependencies + +# With uv +uv sync # installs all dependencies +uv sync --dev # includes dev dependencies +``` + +### uv.lock + +When using `uv`, a `uv.lock` file is generated automatically. It pins every dependency (including transitive ones) to an exact version. Commit this file to version control — it ensures everyone on the team, and your CI pipeline, installs exactly the same packages. + +```bash +# Install from lock file (exact versions) +uv sync +``` + +### Which Format Should You Use? + +| Situation | Recommendation | +|-----------|---------------| +| Quick script or personal project | `requirements.txt` | +| Package or shared project | `pyproject.toml` | +| Using uv | `pyproject.toml` + `uv.lock` | +| Contributing to a project | Use whatever format it already has | + +### Practice Exercises + +* Activate your virtual environment, install `requests` and `pandas`, then run `pip freeze > requirements.txt`. Inspect the file. +* Delete your `.venv`, recreate it, and install from `requirements.txt`. Verify both packages are present. +* Create a minimal `pyproject.toml` for a project with `requests` as a dependency. +* If you completed the `uv` lesson, run `uv add pytest` and inspect the updated `pyproject.toml` and `uv.lock`. diff --git a/python_101/Grade 9 (Extras)/Pandas/01 Pandas.md b/python_101/Grade 9 (Extras)/Pandas/01 Pandas.md new file mode 100644 index 0000000..6009e8e --- /dev/null +++ b/python_101/Grade 9 (Extras)/Pandas/01 Pandas.md @@ -0,0 +1,143 @@ +# Pandas + +### Introduction +Pandas is the most widely used Python library for working with tabular data. It provides two core data structures — `Series` (a single column) and `DataFrame` (a table of rows and columns) — along with a comprehensive set of tools for loading, cleaning, transforming, and analysing data. + +Install it first: + +```bash +pip install pandas +# or with uv +uv add pandas +``` + +### Importing + +```python +import pandas as pd +``` + +### Creating a DataFrame + +```python +import pandas as pd + +data = { + "firstname": ["Alice", "Bob", "Charlie", "Diana"], + "department": ["Engineering", "Finance", "Engineering", "HR"], + "salary": [80000, 65000, 75000, 55000], +} + +df = pd.DataFrame(data) +print(df) +``` + +**Output** + +``` + firstname department salary +0 Alice Engineering 80000 +1 Bob Finance 65000 +2 Charlie Engineering 75000 +3 Diana HR 55000 +``` + +### Reading Data + +```python +# From CSV +df = pd.read_csv("employees.csv") + +# From JSON +df = pd.read_json("employees.json") + +# First look at the data +print(df.head()) # first 5 rows +print(df.tail(3)) # last 3 rows +print(df.shape) # (rows, columns) +print(df.columns) # column names +print(df.dtypes) # data types +print(df.info()) # summary of structure and nulls +print(df.describe()) # statistics for numeric columns +``` + +### Selecting Data + +```python +# Select a single column (returns a Series) +print(df["salary"]) + +# Select multiple columns (returns a DataFrame) +print(df[["firstname", "salary"]]) + +# Select rows by condition +high_earners = df[df["salary"] > 70000] + +# Multiple conditions +engineers = df[(df["department"] == "Engineering") & (df["salary"] > 70000)] + +# Select by row index — iloc uses integer positions +print(df.iloc[0]) # first row +print(df.iloc[0:3]) # first three rows + +# Select by label — loc uses index labels +print(df.loc[0, "salary"]) +``` + +### Adding and Modifying Columns + +```python +# New column from calculation +df["tax"] = df["salary"] * 0.2 +df["net_salary"] = df["salary"] - df["tax"] + +# Update values conditionally +df.loc[df["department"] == "Engineering", "salary"] = df["salary"] * 1.05 +``` + +### Handling Missing Data + +```python +print(df.isnull().sum()) # count nulls per column +df_clean = df.dropna() # remove rows with any null +df_filled = df.fillna(0) # replace nulls with 0 +df["salary"] = df["salary"].fillna(df["salary"].mean()) +``` + +### Grouping and Aggregation + +```python +# Group by department, calculate mean salary +dept_summary = df.groupby("department")["salary"].mean() +print(dept_summary) + +# Multiple aggregations +summary = df.groupby("department").agg( + headcount=("firstname", "count"), + avg_salary=("salary", "mean"), + total_salary=("salary", "sum"), +) +print(summary) +``` + +### Sorting + +```python +df_sorted = df.sort_values("salary", ascending=False) +df_sorted = df.sort_values(["department", "salary"], ascending=[True, False]) +``` + +### Writing Data + +```python +df.to_csv("output.csv", index=False) +df.to_json("output.json", orient="records", indent=2) +``` + +### Practice Exercises + +* Load the `employees.csv` you created in Grade 5 into a DataFrame. Print its shape and column names. +* Filter the DataFrame to only show employees in `Engineering`. +* Add a `tax` column (20% of salary) and a `net_salary` column. +* Group by `department` and calculate the average and total salary per department. +* Sort by salary descending and write the result to a new CSV file. diff --git a/python_101/Grade 9 (Extras)/Polars/01 Polars.md b/python_101/Grade 9 (Extras)/Polars/01 Polars.md new file mode 100644 index 0000000..aacd26a --- /dev/null +++ b/python_101/Grade 9 (Extras)/Polars/01 Polars.md @@ -0,0 +1,163 @@ +# Polars + +### Introduction +Polars is a fast, modern DataFrame library written in Rust. It is designed as a high-performance alternative to Pandas, with a cleaner API, better memory efficiency, and significantly faster execution on large datasets. It is increasingly used in data engineering roles alongside or instead of Pandas. + +Install it first: + +```bash +pip install polars +# or with uv +uv add polars +``` + +### Importing + +```python +import polars as pl +``` + +### Creating a DataFrame + +```python +import polars as pl + +df = pl.DataFrame({ + "firstname": ["Alice", "Bob", "Charlie", "Diana"], + "department": ["Engineering", "Finance", "Engineering", "HR"], + "salary": [80000, 65000, 75000, 55000], +}) + +print(df) +``` + +**Output** + +``` +shape: (4, 3) +┌───────────┬─────────────┬────────┐ +│ firstname ┆ department ┆ salary │ +│ --- ┆ --- ┆ --- │ +│ str ┆ str ┆ i64 │ +╞═══════════╪═════════════╪════════╡ +│ Alice ┆ Engineering ┆ 80000 │ +│ Bob ┆ Finance ┆ 65000 │ +│ Charlie ┆ Engineering ┆ 75000 │ +│ Diana ┆ HR ┆ 55000 │ +└───────────┴─────────────┴────────┘ +``` + +### Reading Data + +```python +df = pl.read_csv("employees.csv") +df = pl.read_json("employees.json") +df = pl.read_parquet("employees.parquet") # Polars shines with parquet + +print(df.shape) +print(df.columns) +print(df.dtypes) +print(df.head()) +print(df.describe()) +``` + +### Selecting and Filtering + +Polars uses an **expression-based API** — operations are built up as expressions and executed together, which is why it is so fast. + +```python +# Select columns +df.select(["firstname", "salary"]) + +# Filter rows +df.filter(pl.col("salary") > 70000) + +# Multiple conditions +df.filter( + (pl.col("department") == "Engineering") & (pl.col("salary") > 70000) +) + +# Select and filter together +df.select(["firstname", "salary"]).filter(pl.col("salary") > 70000) +``` + +### Adding and Transforming Columns + +```python +# with_columns adds or replaces columns +df = df.with_columns([ + (pl.col("salary") * 0.2).alias("tax"), + (pl.col("salary") * 0.8).alias("net_salary"), +]) + +# Rename +df = df.rename({"firstname": "first_name"}) + +# Cast a column type +df = df.with_columns(pl.col("salary").cast(pl.Float64)) +``` + +### Grouping and Aggregation + +```python +summary = df.group_by("department").agg([ + pl.col("firstname").count().alias("headcount"), + pl.col("salary").mean().alias("avg_salary"), + pl.col("salary").sum().alias("total_salary"), +]) + +print(summary) +``` + +### Sorting + +```python +df.sort("salary", descending=True) +df.sort(["department", "salary"], descending=[False, True]) +``` + +### Lazy Evaluation + +Polars has a lazy API that optimises your entire query before running it — similar to how a SQL engine works. This is where its performance advantage is most pronounced. + +```python +result = ( + pl.scan_csv("employees.csv") # lazy — nothing runs yet + .filter(pl.col("salary") > 60000) + .group_by("department") + .agg(pl.col("salary").mean().alias("avg_salary")) + .sort("avg_salary", descending=True) + .collect() # executes the whole plan here +) + +print(result) +``` + +### Writing Data + +```python +df.write_csv("output.csv") +df.write_json("output.json") +df.write_parquet("output.parquet") +``` + +### Polars vs Pandas + +| | Pandas | Polars | +|-|--------|--------| +| Speed on large data | Moderate | Much faster | +| Memory usage | Higher | Lower | +| API style | Index-based | Expression-based | +| Lazy execution | ❌ | ✅ | +| Null handling | `NaN` (inconsistent) | `null` (consistent) | +| Maturity | Very mature | Growing fast | + +> **Which should you use?** Pandas is more established and better supported by older tooling. Polars is the better choice for new projects, especially those dealing with large files or performance-sensitive pipelines. It is worth knowing both. + +### Practice Exercises + +* Load the same `employees.csv` you used in the Pandas lesson into a Polars DataFrame. Print its shape and column names. +* Filter to only `Engineering` employees earning more than `70000`. +* Add `tax` and `net_salary` columns using `with_columns`. +* Group by `department`, returning headcount and average salary. +* Write a lazy query using `scan_csv` that filters, groups, and sorts — then collect and print the result. diff --git a/python_101/Intro.md b/python_101/Intro.md new file mode 100644 index 0000000..3b1a9cd --- /dev/null +++ b/python_101/Intro.md @@ -0,0 +1,95 @@ +--- +id: intro +sidebar_position: 1 +--- +# Intro + +The Python 101 course is designed for someone aspiring to build a career as a data engineer, analyst, or data scientist. This course covers fundamental Python concepts, from writing your first script through to building reusable code, working with data, and handling errors like a professional. + +By the end of the course, you will be writing clean, structured Python programs, working with real data, and using Python to solve practical problems. + +If you like this course and want to support the project, you can do so here: + +[![Support me on Ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/tomfynes) + +## Recommended Tools + +* [VS Code](https://code.visualstudio.com/) with the [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python) +* [Python 3.10+](https://www.python.org/downloads/) + +You may also use any other editor or IDE you feel comfortable with, such as PyCharm or Jupyter Notebook. + +## Setup + +To participate please fork the [repo](https://github.com/Tom-Fynes/python-101) and follow one of the setup documents. Please save your exercises [here](https://github.com/Tom-Fynes/python-101/tree/main/exercises). Also while going through the course if you find an issue or think of an improvement please log an issue [here](https://github.com/Tom-Fynes/python-101/issues). + +* [Windows](/python_101/Getting%20Started/Windows) +* [macOS](/python_101/Getting%20Started/macOS) +* [Linux](/python_101/Getting%20Started/Linux) + +## Grades + +Grades have been added to help break your journey into helpful sections. Each section will cover different skill levels and will help you demonstrate your ability to other people. + +### Grade 1: Introduction to Python + +* [**What is Python**](/python_101/Grade%201/01%20What%20is%20Python): What Python is and why it matters. +* [**Your First Script**](/python_101/Grade%201/02%20Your%20first%20script): Writing and running a Python file. +* [**Variables and Data Types**](/python_101/Grade%201/03%20Variables%20and%20data%20types): Storing values with the right type. +* [**User Input and Print**](/python_101/Grade%201/04%20User%20input%20and%20print): Getting input and displaying output. + +### Grade 2: Control Flow + +* [**Comparison and Logical Operators**](/python_101/Grade%202/01%20Operators): Using `==`, `!=`, `and`, `or`, `not`. +* [**If Statements**](/python_101/Grade%202/02%20If%20statements): Making decisions with `if`, `elif`, and `else`. +* [**For Loops**](/python_101/Grade%202/03%20For%20loops): Iterating with `for` and `range`. +* [**While Loops**](/python_101/Grade%202/04%20While%20loops): Repeating code with `while`. + +### Grade 3: Data Structures + +* [**Lists**](/python_101/Grade%203/01%20Lists): Creating and manipulating ordered collections. +* [**Tuples**](/python_101/Grade%203/02%20Tuples): Immutable sequences and when to use them. +* [**Dictionaries**](/python_101/Grade%203/03%20Dictionaries): Key-value data storage and lookup. +* [**Sets**](/python_101/Grade%203/04%20Sets): Unique collections and set operations. + +### Grade 4: Functions + +* [**Defining Functions**](/python_101/Grade%204/01%20Defining%20functions): Writing reusable blocks of code with `def`. +* [**Parameters and Return Values**](/python_101/Grade%204/02%20Parameters%20and%20return%20values): Passing data in and out of functions. +* [**Scope**](/python_101/Grade%204/03%20Scope): Understanding local and global variables. +* [**Lambda Functions**](/python_101/Grade%204/04%20Lambda%20functions): Writing short anonymous functions. + +### Grade 5: Working with Files and Modules + +* [**Reading and Writing Files**](/python_101/Grade%205/01%20Reading%20and%20writing%20files): Opening, reading, writing, and closing files. +* [**Working with CSV Files**](/python_101/Grade%205/02%20CSV%20files): Reading and writing CSV data with the `csv` module. +* [**Importing Modules**](/python_101/Grade%205/03%20Importing%20modules): Using Python's standard library. +* [**Creating Your Own Modules**](/python_101/Grade%205/04%20Creating%20modules): Organising code across multiple files. + +### Grade 6: Error Handling and Debugging + +* [**Errors and Exceptions**](/python_101/Grade%206/01%20Errors%20and%20exceptions): Understanding what goes wrong and why. +* [**Try and Except**](/python_101/Grade%206/02%20Try%20and%20except): Catching and handling exceptions gracefully. +* [**Raising Exceptions**](/python_101/Grade%206/03%20Raising%20exceptions): Throwing your own errors when needed. +* [**Debugging Techniques**](/python_101/Grade%206/04%20Debugging): Using print statements, breakpoints, and reading tracebacks. + +### Grade 7: Object-Oriented Programming + +* [**Classes and Objects**](/python_101/Grade%207/01%20Classes%20and%20objects): Defining classes and creating instances. +* [**Attributes and Methods**](/python_101/Grade%207/02%20Attributes%20and%20methods): Storing state and defining behaviour. +* [**Inheritance**](/python_101/Grade%207/03%20Inheritance): Building on existing classes. +* [**Dunder Methods**](/python_101/Grade%207/04%20Dunder%20methods): Customising built-in behaviour with `__init__`, `__str__`, and others. + +### Grade 8: Package Management and Virtual Environments + +* [**What is pip**](/python_101/Grade%208/01%20What%20is%20pip): Installing and managing packages. +* [**Virtual Environments**](/python_101/Grade%208/02%20Virtual%20environments): Isolating project dependencies with `venv`. +* [**uv**](/python_101/Grade%208/03%20uv): A faster alternative for managing packages and environments. +* [**requirements.txt and pyproject.toml**](/python_101/Grade%208/04%20Dependency%20files): Tracking and sharing your project's dependencies. + +### Grade 9: Extras + +In this section we will cover library-specific topics that are widely used in data roles. These are optional extensions — go as deep as you like. + +* [**Pandas**](/python_101/Grade%209%20(Extras)/Pandas): Tabular data manipulation with DataFrames. +* [**Polars**](/python_101/Grade%209%20(Extras)/Polars): A fast, modern alternative to Pandas. diff --git a/python_101/intro.md b/python_101/intro.md deleted file mode 100644 index a62f2df..0000000 --- a/python_101/intro.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -id: intro -sidebar_position: 1 ---- - -# Python 101 -Coming Soon!7 - -A course is designed for developers, data professionals, and anyone interested in mastering Python programming. By the end of this course, you'll be proficient in Python and ready to tackle real-world projects. - - diff --git a/src/pages/career.js b/src/pages/career.js index 30ec50b..a476e1a 100644 --- a/src/pages/career.js +++ b/src/pages/career.js @@ -19,7 +19,7 @@ const ACCENT = "#DD7596"; const PERIWINKLE = "#B7C3F3"; const PALE_BLUE = "#AED6F1"; const MAGENTA = "#ECDA90"; -const CONTACT_EMAIL = "tf.dev@icloud.com"; +const CONTACT_EMAIL = "contact@fynesforge.dev"; // ─── Data ────────────────────────────────────────────────────────────────────── diff --git a/src/pages/index.js b/src/pages/index.js index cc50803..4ef126d 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -189,7 +189,7 @@ export function HomepageHeader() {