From 0cf0ca496b96743408d4ce9714e645361bf41330 Mon Sep 17 00:00:00 2001 From: Academy CI Date: Fri, 14 Aug 2026 01:42:12 +0300 Subject: [PATCH] Sync content from the monorepo --- course/module-0/intro-community/article.md | 25 +- course/module-0/intro-intro/article.md | 49 +- .../intro-structure-of-course/article.md | 49 +- .../module-0/python-installation/article.md | 123 +++-- course/module-1/basic-data-types/article.md | 98 +++- course/module-1/boolean-data-types/article.md | 249 ++++++---- course/module-1/complex-data-types/article.md | 45 +- course/module-1/dictionaries/article.md | 376 +++++++-------- course/module-1/functions/article.md | 176 +++++-- course/module-1/if-statement/article.md | 287 ++++++++--- course/module-1/lists/article.md | 258 +++++++++- course/module-1/loops/article.md | 183 ++++--- course/module-1/numeric-data-types/article.md | 229 ++++----- course/module-1/sets/article.md | 266 +++++++--- course/module-1/string-data-types/article.md | 453 +++++++++++++----- course/module-1/tuples/article.md | 171 +++++-- course/module-1/variable-scope/article.md | 183 ++++--- course/module-1/variables/article.md | 227 +++++---- course/module-2/attributes/article.md | 302 +++++------- course/module-2/built-in-libraries/article.md | 196 ++++---- course/module-2/classes/article.md | 173 +++++-- course/module-2/datetime-handling/article.md | 174 +++++-- course/module-2/decorators/article.md | 181 ++++--- course/module-2/encapsulation/article.md | 112 +++-- course/module-2/exception-handling/article.md | 254 ++++------ course/module-2/files/article.md | 375 ++++++++------- course/module-2/inheritance/article.md | 106 ++-- course/module-2/lambda-functions/article.md | 230 ++++----- course/module-2/libraries/article.md | 116 +++-- course/module-2/modules/article.md | 315 +++++------- course/module-2/oop/article.md | 145 ++---- course/module-2/polymorphism/article.md | 77 +-- course/module-2/structured-data/article.md | 307 +++++------- course/module-2/text-files/article.md | 239 +++------ .../module-2/third-party-libraries/article.md | 265 +++++----- course/module-2/type-annotations/article.md | 156 +++--- course/module-3/asyncio-advanced/article.md | 61 ++- course/module-3/asyncio-basics/article.md | 27 +- course/module-3/coverage-and-ci/article.md | 39 +- .../intro-concurrency-async/article.md | 72 ++- course/module-3/intro-to-databases/article.md | 112 ++--- course/module-3/intro-to-testing/article.md | 147 +++--- .../module-3/iterations-and-loops/article.md | 331 ------------- course/module-3/mocks-and-stubs/article.md | 65 ++- course/module-3/pytest-basics/article.md | 80 +++- .../article.md | 51 +- .../sqlalchemy-core-basics/article.md | 191 +++++--- .../module-3/sqlalchemy-orm-basics/article.md | 293 +++++++---- .../module-3/sqlite-python-basics/article.md | 191 ++++---- course/module-3/testing/article.md | 40 +- .../threading-and-multiprocessing/article.md | 92 ++-- .../unittest-module-testing/article.md | 112 ++++- course/module-3/unittest-module/article.md | 67 +-- .../module-3/why-tests-are-needed/article.md | 57 ++- interviewQuestions/1/en.mdx | 22 +- interviewQuestions/10/en.mdx | 8 +- interviewQuestions/11/en.mdx | 4 +- interviewQuestions/12/en.mdx | 8 +- interviewQuestions/13/en.mdx | 10 +- interviewQuestions/14/en.mdx | 12 +- interviewQuestions/15/en.mdx | 4 +- interviewQuestions/16/en.mdx | 14 +- interviewQuestions/17/en.mdx | 10 +- interviewQuestions/18/en.mdx | 6 +- interviewQuestions/19/en.mdx | 20 +- interviewQuestions/2/en.mdx | 16 +- interviewQuestions/20/en.mdx | 10 +- interviewQuestions/21/en.mdx | 16 +- interviewQuestions/22/en.mdx | 12 +- interviewQuestions/23/en.mdx | 10 +- interviewQuestions/24/en.mdx | 4 +- interviewQuestions/25/en.mdx | 10 +- interviewQuestions/26/en.mdx | 4 +- interviewQuestions/27/en.mdx | 10 +- interviewQuestions/28/en.mdx | 14 +- interviewQuestions/29/en.mdx | 14 +- interviewQuestions/3/en.mdx | 22 +- interviewQuestions/30/en.mdx | 10 +- interviewQuestions/31/en.mdx | 2 +- interviewQuestions/32/en.mdx | 10 +- interviewQuestions/33/en.mdx | 26 +- interviewQuestions/34/en.mdx | 12 +- interviewQuestions/35/en.mdx | 8 +- interviewQuestions/36/en.mdx | 14 +- interviewQuestions/37/en.mdx | 10 +- interviewQuestions/38/en.mdx | 12 +- interviewQuestions/39/en.mdx | 10 +- interviewQuestions/4/en.mdx | 16 +- interviewQuestions/40/en.mdx | 24 +- interviewQuestions/5/en.mdx | 10 +- interviewQuestions/6/en.mdx | 24 +- interviewQuestions/7/en.mdx | 14 +- interviewQuestions/8/en.mdx | 10 +- interviewQuestions/9/en.mdx | 10 +- 94 files changed, 5285 insertions(+), 4395 deletions(-) delete mode 100644 course/module-3/iterations-and-loops/article.md diff --git a/course/module-0/intro-community/article.md b/course/module-0/intro-community/article.md index ec85364..a22cb6b 100644 --- a/course/module-0/intro-community/article.md +++ b/course/module-0/intro-community/article.md @@ -1,22 +1,19 @@ -# Community - -We understand that communication is a very important part of the educational process, so we offer to join our community, where -you can get help and support from other students or suggest yourself in case of questions from other members of the community. +--- +meta: + title: "Python Academy Community" + description: "The Python Academy chat and channel on Telegram: where to ask when you get stuck, and where to write about service or payment problems." +--- -Communication is organized through: - -- **[community on Telegram](https://telegram.me/pythonacademyofficial)** +# Community -On the community page, we post information on the latest and most important updates. +Getting stuck on your own is easy: the code doesn't work, the error message explains nothing, and there's no one to ask. That's exactly what the **chat on Telegram** is for β€” it has both people taking the course right now and people who finished it a while ago. -There are **[Telegram](https://telegram.me/pythonacademychat)** chat for communication between participants. +There's no shame in asking. Answering is just as useful, by the way: explaining someone else's error is what finally settles the topic in your own head. -## Problem solving +If you'd rather skip the conversation and just follow the course, there's the **channel on Telegram**. It carries important updates and material on Python, data analytics and backend development β€” including how to use them to find a job or get a promotion. -For questions that arise during the operation of the service, comments and recommendations on the content of the course, or if there are problems with paying for a premium account, you can use -the following communication methods: +## When something breaks -- Write a letter to - **[feedback@python-academy.org](mailto:feedback@python-academy.org)** +The chat is good for questions about code. But if you've hit a bug in the service itself, want to correct us on the course content, or have trouble paying for premium β€” write to **[feedback@python-academy.org](mailto:feedback@python-academy.org)**. We are glad to see you in our community 😍 diff --git a/course/module-0/intro-intro/article.md b/course/module-0/intro-intro/article.md index 125aa1c..432931e 100644 --- a/course/module-0/intro-intro/article.md +++ b/course/module-0/intro-intro/article.md @@ -1,15 +1,18 @@ +--- +meta: + title: "Introduction to the Python course" + description: "Free online Python course: how the course, trainer and sandbox fit together, which devices to learn on, and what a premium account is for." +--- + # About the course -This course, aimed at learning Python, is designed differently than most other courses. -We will not try to learn all possible Python libraries, instead we will focus our attention on the principles of programming and -on the foundation of Python fundamentals that will allow you to build any application. +You can watch ten hours of Python videos, nod along, and still not write a single line yourself. That feeling of "it all made sense in the lecture, but I can't sit down and do it" comes from exactly there. + +This course is built the other way around. We don't try to march through every Python library β€” instead we work out how the language itself is put together, and write code right there in the article. You'll pick up whatever library you need in an evening, provided you understand the foundation. It doesn't work in the other direction. -To achieve this goal, the course is equipped with a large number of exercises that should help build the very necessary connections in our brain, so that in the future you do not have -to think much when writing Python code for your applications. +So there will be a lot of exercises ahead. They aren't box-ticking: the connections you need only form when you fight an error yourself, rather than reading someone else's finished solution. And if something drags β€” that's a normal part of the process, not a sign that "programming isn't for you". -During the course, we strongly recommend that you cultivate a "growth mindset": if you encounter any difficulties trying -to understand a new topic or doing exercises, try to transform the frustrating experience into a productive one. -And the most important thing in this course is **have fun!** +And the most important thing β€” **have fun!** ## Take the course publicly @@ -19,28 +22,38 @@ You can use the hashtag `#python-academy` if you want. ## Device support -This course is designed to pass from a computer and laptop - the most optimal devices for writing Python code. The course can also be taken from a tablet or from a phone, -but some functionality may not be available or work incorrectly. Please treat this with understanding. +The course expects a computer or a laptop β€” writing code is simply easier with a keyboard. You can also work from a tablet or a phone, +but some of the functionality will be unavailable or won't behave as intended. Please treat this with understanding. ## How our site is organized -![How our site is organized](https://python-academy.org/static/guidePage/intro-intro/connections.webp 'How our site is organized') +![How our site is organized](https://python-academy.org/static/guidePage/intro-intro/sections-en.webp "How our site is organized") There are 3 main sections on our resource, each of which will be useful in different ways: -- **[Course](/guide)** - is your step-by-step assistant when learning Python. It presents consistent, structured materials, as well as related exercises to consolidate knowledge. -- **[Trainer](/trainer)** the simulator is a tool to test your knowledge. The tasks in it are ranked by difficulty, so you can choose the ones that are suitable for your current level. - Also, solving tasks from the simulator is necessary to obtain a certificate confirming knowledge in Python programming. -- **[Sandbox](/sandbox)** - is a space for free experimentation with Python code and trying out concepts you've learned. +- ** Course ** - is your step-by-step assistant when learning Python. It presents consistent, structured materials, as well as related exercises to consolidate knowledge. +- **Simulator** - is a tool to test your knowledge. The tasks in it are ranked by difficulty, so you can choose the ones that are suitable for your current level. + Also, solving tasks from the simulator is necessary to obtain a certificate confirming knowledge in Python programming. +- **Sandbox ** - is a space for free experimentation with Python code and trying out concepts you've learned. ## Premium account -The purchase of [premium account](/premium) is an optional part, we provide the course for everyone absolutely free of charge. But it opens up more opportunities for you -to study more productively, access to tasks from real interviews at top companies and gives you the opportunity to get a certificate. In addition, this is a great opportunity to support us in the development of this service. +The basic course, trainer and sandbox are free. A premium account is an optional addition for those who want to go deeper and use everything the platform has to offer. + +With a premium account you get: + +- access to reference solutions for trainer tasks; +- the advanced courses "Backend on FastAPI from scratch" and "Product analytics with Python"; +- extended access to the AI assistant and tasks from interviews at top companies; +- the ability to issue personalised certificates. + +Premium lasts 6 months from the date of payment. All the progress you made before the purchase is kept and still counts towards your certificates. Once premium expires, the account becomes free again, while your progress and any certificates you have already issued stay with you. + +By buying a premium account you also support the development of Python Academy and help us create new learning materials and features. ## Progress and certificate acquisition -If you are logged in, you can see the progress bar in the **[trainer](/trainer)** section. +If you are logged in, you can see the progress bar in the **trainer** section. ![Progress bar before receiving a certificate](https://python-academy.org/static/guidePage/intro-intro/progress_bar_en.jpg) diff --git a/course/module-0/intro-structure-of-course/article.md b/course/module-0/intro-structure-of-course/article.md index 141f990..e09eeac 100644 --- a/course/module-0/intro-structure-of-course/article.md +++ b/course/module-0/intro-structure-of-course/article.md @@ -1,34 +1,47 @@ -# Course structure - -This course is divided into 3 modules, not counting the introduction module. Each module can be considered as a mini-course, -which aims to study a certain group of knowledge in the field of Python. +--- +meta: + title: "Course structure" + description: "What the Python course is made of: three modules, from variables to async. How executable code blocks and self-testing tasks work." +--- -Although the modules can be completed independently of each other, we still recommend taking the course in order. +# Course structure -At the end of most practical topics there is a block "Tasks for self-testing" so that you can immediately practice using -new knowledge. +The course is three modules, not counting the introduction you are reading right now. Each module works like a mini-course: it takes its own group of topics and sees them through. -## Interfaces +- **Fundamentals of Python I** β€” variables, data types, functions, conditions, loops and collections. The things every Python program is built from. +- **Fundamentals of Python II** β€” libraries and modules, working with files, classes and objects, exceptions, decorators, dates. +- **Advanced Python** β€” databases, testing, threading and async. -During the course of this course, you will meet possibly non-standard components, so it is described below how to interact with them. +The order is not arbitrary: each module stands on the one before it. In the second you will write functions and loop over lists from the first, and the third does not hold together without them. If the basics are already familiar, skim what you know β€” but don't start in the middle. -### Block of executable code +## The code block -Perhaps the most popular component will be the executable code block, it looks like this: +Almost every article has a code block in it. It looks like this: -```python-executable +```python print("Hi, I love Python 😊") + +Hi, I love Python 😊 + ``` -If it has an active blue button, then this code can be executed. When you click on the request execution button, the code will open in a new tab in the sandbox. +The line marked with `>_` is not code any more β€” it's the result: what Python printed after running the line above. So read the block top to bottom: first what we asked for, then what came back. + +Four icons live in the top right of the block: + +![Executable code block](https://python-academy.org/static/guidePage/intro-structure-of-course/code-block-en.webp "Executable code block") + +The β–Ά icon isn't on every block β€” only on the ones whose code can be run. It opens the code in the **sandbox**: a separate tab where you can change it and run it as many times as you like. + +One detail about β–Ά: it sends only its own block to the sandbox, nothing more. So every example is self-contained β€” everything the code needs is declared right in it, and you can run the blocks in any order. -![Description of the block with executable code](https://python-academy.org/static/guidePage/intro-structure-of-course/en_codeplayground_description.png 'Description of the block with executable code') +## Self-testing tasks -### Self-testing tasks +Not in every article, but in many β€” especially in "Fundamentals of Python I" β€” a block of tasks waits at the end: -It is strongly recommended not to skip the tasks at the end of each article, they look like this: +![Self-testing tasks](https://python-academy.org/static/guidePage/intro-structure-of-course/en_exercises_description.png "Self-testing tasks") -![Description of self-test tasks](https://python-academy.org/static/guidePage/intro-structure-of-course/en_exercises_description.png 'Description of self-test tasks') +Don't skip them. Reading about a loop and writing a loop are different things, and you only find out which is which at the keyboard πŸŽ“. -It is recommended to solve problems while logged in, so all your solutions will be saved. +Log in before you start, otherwise your solutions won't be saved. diff --git a/course/module-0/python-installation/article.md b/course/module-0/python-installation/article.md index 9ff288e..d32c90c 100644 --- a/course/module-0/python-installation/article.md +++ b/course/module-0/python-installation/article.md @@ -1,120 +1,107 @@ -# Installing Python - -For this course, installing Python locally is optional: all suggested tasks run on our servers, and any custom Python code can be executed in the **[Sandbox](/sandbox)**. +--- +meta: + title: "Installing Python" + description: "How to install Python 3 on Windows, macOS or Linux: a step-by-step guide, the Add to PATH checkbox, and checking that it all works." +--- -If you still want to run code locally, here is a short installation guide. - -In this quick guide you will install the latest Python 3 for your system and verify everything works. Let’s go! πŸš€ +# Installing Python -## Step 1. Check if Python is already installed +You don't have to install Python for this course: the tasks run on our servers, and any code can be executed in the **Sandbox**. So if you're itching to start β€” skip ahead, this chapter isn't going anywhere. -Open your terminal/console and check the version commands. +But sooner or later you'll want to write code on your own machine: your files, your projects, no browser tab involved. Come back here then β€” below is the short path from "nothing installed" to "it works" πŸš€. -- **Windows (PowerShell or CMD):** +## Step 1. Check whether Python is already there - ```bash - python --version - py --version - pip --version - ``` +Python may have shipped with your system or been installed earlier β€” on macOS and Linux it's there almost always. We'll check in the terminal: the window where you type commands as text instead of clicking. -- **macOS / Linux (Terminal):** +- **Windows** β€” press `Win`, type "PowerShell" and open it. +- **macOS** β€” press `Cmd + Space`, type "Terminal" and open it. +- **Linux** β€” usually `Ctrl + Alt + T`. - ```bash - python3 --version - pip3 --version - ``` +Type the command and press `Enter`: -If you see a version (e.g., `Python 3.12.x`), Python is already installed. If commands are not found β€” proceed to installation. +**macOS / Linux** -## Windows installation +```sh +python3 --version +``` -1. Download the official installer: **[python.org β†’ Downloads β†’ Windows](https://www.python.org/downloads/windows/)**. -2. Run the installer. -3. On the first screen, tick **β€œAdd python.exe to PATH”** (important!) and click **Install Now**. -4. Wait for completion and close the installer. -5. Verify in PowerShell/CMD: +**Windows** -```bash -py --version +```powershell python --version -pip --version ``` -If `python` is not found, try `py`, or sign out/in. Reboot if necessary. +If a version number comes back β€” say, `Python 3.12.4` β€” it's already in place and you can skip step 2. Any version starting with a 3 will do. -## macOS installation +A "command not found" answer is fine too: it just means Python isn't installed yet. Let's move on. -Two convenient options β€” pick one. +## Step 2. Install Python -### Option A: Official installer +### Windows -1. Download from **[python.org β†’ Downloads β†’ macOS](https://www.python.org/downloads/macos/)**. -2. Open the `.pkg` and follow the installer. -3. Verify versions: +1. Open **python.org β†’ Downloads β†’ Windows** and download the installer. +2. Run it. +3. On the very first screen tick **"Add python.exe to PATH"** β€” and only then click **Install Now**. +4. Wait for it to finish and close the installer. -```bash -python3 --version -pip3 --version -``` +That checkbox deserves a word of its own, because it's easy to miss and the consequences are annoying. PATH is the list of folders your system searches for programs when you type a command. Without the tick, Python installs but the system never learns about it: you type `python` and get "command not found" back. Ticking it takes a second; hunting down the reason afterwards can take half an hour. -### Option B: Homebrew +If you did miss it, the easiest fix is to run the installer again and choose **Modify**. -1. Make sure Homebrew is installed (`brew -v`). If not, follow **[brew.sh](https://brew.sh/)**. -2. Install Python: +### macOS -```bash -brew install python -``` +The first way, with no extra tooling: download the installer from **python.org β†’ Downloads β†’ macOS**, open the `.pkg` and follow the steps. -3. Verify versions: +The second way is **Homebrew**, if you already have it (check with `brew -v`): ```bash -python3 --version -pip3 --version +brew install python ``` -Note: on macOS, the `python3` command (not `python`) is standard β€” this is expected. +### Linux -## Linux installation +The command depends on your distribution: -Open Terminal and run commands for your distribution. - -- **Ubuntu / Debian:** +- **Ubuntu / Debian:** ```bash sudo apt update sudo apt install -y python3 python3-pip ``` -- **Fedora:** +- **Fedora:** ```bash sudo dnf install -y python3 python3-pip ``` -- **Arch Linux:** +- **Arch Linux:** ```bash sudo pacman -S python python-pip ``` -Verify versions: +## Step 3. Check that it works -```bash +Close the terminal and open it again β€” it needs to learn about the new program. Now the same command as in step 1: + +**macOS / Linux** + +```sh python3 --version -pip3 --version ``` -## FAQ +**Windows** -- **`python` command not found.** On Windows try `py`. On macOS/Linux use `python3` and `pip3` β€” that’s normal. -- **Upgrade `pip`:** +```powershell +python --version +``` - ```bash - python -m pip install --upgrade pip - # or - python3 -m pip install --upgrade pip - ``` +A version number in reply means you're done, Python is yours. + +If Windows still says "command not found", try `py --version`. If that doesn't help, it's almost certainly the PATH checkbox: run the installer again and choose **Modify**. Sometimes simply logging out and back in is enough. + +On macOS and Linux the command is `python3`, not `python` β€” that's not a fault but the norm: the short name `python` is historically taken there. -Good luck with learning Python! ✨ +Good luck! ✨ diff --git a/course/module-1/basic-data-types/article.md b/course/module-1/basic-data-types/article.md index ab10d69..868fb0c 100644 --- a/course/module-1/basic-data-types/article.md +++ b/course/module-1/basic-data-types/article.md @@ -1,38 +1,84 @@ +--- +meta: + title: "Basic Data Types in Python" + description: "Why Python attaches a type to every value: how the same operator behaves differently, how to inspect a type with type, and an overview of built-in types." +--- + # Basic Data Types in Python -Data type is an important concept in programming that defines the characteristics of data, -their possible values, and operations that can be performed with them. +Look at these two lines. The operator is the same, but the result is different: + +```python +print(10 + 5) + +15 + + +print("10" + "5") + +105 + +``` + +In the first case `+` added the numbers; in the second it glued two strings together. Python behaves differently because `10` and `"10"` are not the same thing to it: the first is a number, the second is text. Python attaches a **type** to every value, and it's the type that decides what an operation on that value means. + +That's why mixing up types is a common source of beginner bugs: you add two values expecting 15 and get "105" instead, because the values were actually strings. + +## How to find out a value's type + +If you're not sure what you're dealing with, you can ask for the type directly with the `type()` function. -Variables can store data of different types, and different types can perform -different actions. +```python +print(type(42)) + + + + +print(type(3.14)) + + + + +print(type("Python")) + + + + +print(type(True)) + + + +``` -For example, strings can perform concatenation operations, -while numbers can perform arithmetic operations. +In the output `int` is a whole number, `float` is a number with a fractional part, `str` is a string, `bool` is a boolean value (true or false). You don't declare the type up front: Python figures it out from what you wrote. ```python -number = 42 # integer -pi = 3.14 # float -name = "Python" # string -is_active = True # boolean +number = 42 # int β€” a whole number +pi = 3.14 # float β€” a number with a fractional part +name = "Python" # str β€” a string +is_active = True # bool β€” a boolean value ``` -## Built-in Data Types in Python +## What types exist in Python + +Besides the four basic ones, Python has built-in types for collections and more specialized tasks. Here are the main categories: + +| Category | Types | +| ---------------- | ------------------------------ | +| Text | `str` | +| Numbers | `int`, `float` | +| Boolean | `bool` | +| Collections | `list`, `tuple`, `set`, `dict` | +| Absence of value | `NoneType` (the value `None`) | + +In the next few chapters we'll cover numbers, strings, and booleans in detail, then move on to collections β€” lists, tuples, sets, and dictionaries. For now one idea is enough: every value has a type, and that type decides what you can do with the value. + +## Check your understanding -Python has the following data types built-in by default: +**What does `print("3" + "4")` output?** -| Category | Data Types | -| -------------- | ---------------------------------- | -| Text Type | `str` | -| Numeric Types | `int`, `float`, `complex` | -| Sequence Types | `list`, `tuple`, `range` | -| Mapping Type | `dict` | -| Set Types | `set`, `frozenset` | -| Boolean Type | `bool` | -| Binary Types | `bytes`, `bytearray`, `memoryview` | -| None Type | `NoneType` | +1. 7 β€” The numbers would add up to 7 if they were numbers: 3 + 4. But in quotes these are strings, and for strings + means joining, not addition. -This makes Python very flexible and convenient for rapid development, but -it's important to understand the basic data types and their features to write efficient code and -avoid errors. +2. **Correct answer:** 34 β€” For strings the + operator joins them one after another, so "3" and "4" give "34". To get 7 the values would have to be numbers: 3 + 4. -In the future, we will definitely look at each of these types in more detail. +3. An error: you can't add strings β€” Adding strings is in fact allowed β€” it's the join operation. An error would come from adding a string to a number, for example "3" + 4. diff --git a/course/module-1/boolean-data-types/article.md b/course/module-1/boolean-data-types/article.md index 6f57ba5..31c8ca5 100644 --- a/course/module-1/boolean-data-types/article.md +++ b/course/module-1/boolean-data-types/article.md @@ -1,98 +1,107 @@ -# Boolean Data in Python +--- +meta: + title: "Boolean data in Python" + description: "Detailed description of the boolean data type in Python: creating boolean values, operations, type conversion, and practical usage examples." +--- -Let's dive into the world of boolean data in Python. -This topic may seem simple, but it's incredibly important β€” after all, it's with boolean values that your programs will make decisions! 🧠 +# Boolean data in Python + +Whenever a program decides what to do next, whether to print a message, whether to let a user in, whether to retry a request, it works with boolean values. Python's `bool` type holds exactly two values: `True` and `False`, and every condition and check is built from them. ## What is boolean data? -In everyday life, we often ask questions with "yes" or "no" answers. -For example: "Is it raining today?", "Do you like pizza?", "Is 2 + 2 = 5?". +In Python, the values "true" and "false" are represented by the `bool` type, which can hold only two values: -In Python, there's a special data type for such answers β€” `bool`, which can only take two values: +- `True` +- `False` -- `True` (truth, "yes") βœ… -- `False` (falsehood, "no") ❌ +Ask Python for the type and it confirms you're looking at a `bool`: ```python -# Creating boolean variables -is_raining = True # Yes, it's raining -likes_pizza = True # Yes, I like pizza - -print(type(is_raining)) # +is_raining = True +print(type(is_raining)) + + + ``` -> πŸ’‘ **Important**: `True` and `False` are always written with a capital letter. -> If you write `true` or `false`, Python won't understand and will raise an error! - -## Boolean operators: how to combine conditions +> **Important:** `True` and `False` are always written with a capital first letter. If you write `true` or `false`, Python won't understand them and will throw an error. -In life, we often combine multiple conditions. -For example: "I'll go to the beach if it's sunny **AND** warm" or "I'll buy this phone if it's beautiful **OR** inexpensive". +## Boolean operators -Python gives us three main logical operators: +We often need to combine several conditions. For example: "I'll go to the beach if it's sunny **AND** warm" or "I'll buy this phone if it's beautiful **OR** cheap". Python gives us three operators for this: `and`, `or`, `not`. -### 1. The and operator (logical AND) +### The and operator (logical AND) Returns `True` only if **both** values are true: ```python -sunny = True # It's sunny -warm = True # It's warm +sunny = True +warm = True -# I'll go to the beach if it's sunny AND warm +# Go to the beach if sunny AND warm going_to_beach = sunny and warm print(f"Sunny: {sunny}, Warm: {warm}") + +Sunny: True, Warm: True + print(f"Going to the beach? {going_to_beach}") + +Going to the beach? True + # What if the weather changes? warm = False # It got cold going_to_beach = sunny and warm print(f"Sunny: {sunny}, Warm: {warm}") + +Sunny: True, Warm: False + print(f"Going to the beach? {going_to_beach}") + +Going to the beach? False + ``` -### 2. The or operator (logical OR) +### The or operator (logical OR) Returns `True` if **at least one** value is true: ```python -phone_is_beautiful = True # The phone is beautiful -phone_is_cheap = False # But not cheap +phone_is_beautiful = True +phone_is_cheap = False -# I'll buy the phone if it's beautiful OR inexpensive +# Buy the phone if it's beautiful OR cheap will_buy_phone = phone_is_beautiful or phone_is_cheap print(f"Phone is beautiful: {phone_is_beautiful}, Phone is cheap: {phone_is_cheap}") -print(f"Will buy the phone? {will_buy_phone}") -``` - -Truth table for the `or` operator: - -```python -print("True or True =", True or True) -print("True or False =", True or False) -print("False or True =", False or True) -print("False or False =", False or False) + +Phone is beautiful: True, Phone is cheap: False + +print(f"Will we buy the phone? {will_buy_phone}") + +Will we buy the phone? True + ``` -### 3. The not operator (logical NOT) +### The not operator (logical NOT) Inverts the value: `True` becomes `False`, and vice versa: ```python have_homework = True print(f"I have homework: {have_homework}") -print(f"I DON'T have homework: {not have_homework}") - -# Another example: if it's not raining, we go for a walk -is_raining = False -going_for_a_walk = not is_raining -print(f"Is it raining? {is_raining}") -print(f"Going for a walk? {going_for_a_walk}") + +I have homework: True + +print(f"I do NOT have homework: {not have_homework}") + +I do NOT have homework: False + ``` ## Comparing values -Boolean values often appear as a result of comparison: +Boolean values often come up as the result of a comparison: ```python # Comparing numbers @@ -100,104 +109,155 @@ my_age = 25 friend_age = 30 print(f"My age: {my_age}, friend's age: {friend_age}") -print(f"Our ages are the same? {my_age == friend_age}") -print(f"Our ages are different? {my_age != friend_age}") + +My age: 25, friend's age: 30 + +print(f"Are we the same age? {my_age == friend_age}") + +Are we the same age? False + +print(f"Are we different ages? {my_age != friend_age}") + +Are we different ages? True + print(f"Am I younger? {my_age < friend_age}") -print(f"Am I older? {my_age > friend_age}") + +Am I younger? True + # Comparing strings (alphabetically) -print("String comparison:") print(f"'apple' < 'banana': {'apple' < 'banana'}") -print(f"'python' == 'Python': {'python' == 'Python'}") + +'apple' < 'banana': True + +``` -# Comparing lists -my_scores = [90, 85, 95] -friend_scores = [90, 85, 95] # Same list, but different object -same_list = my_scores # The same object +### The is operator: comparing identity -print("List comparison:") -print(f"Contents the same? {my_scores == friend_scores}") -print(f"Is it the same object? {my_scores is friend_scores}") +Beyond `==`, Python also has the `is` operator. They look similar but check different things: + +- `==` compares **values** (what's inside) +- `is` compares **identity** (whether two names point at the same object in memory) + +```python +my_scores = [90, 85, 95] +friend_scores = [90, 85, 95] # Same list, but a different object +same_list = my_scores # The same object + +print(f"Same content? {my_scores == friend_scores}") + +Same content? True + +print(f"The same object? {my_scores is friend_scores}") + +The same object? False + print(f"Is same_list the same object as my_scores? {my_scores is same_list}") + +Is same_list the same object as my_scores? True + ``` -## Operator precedence: what gets calculated first? +**Rule of thumb:** in the vast majority of cases you want `==`. The `is` operator is appropriate only for checks against the special singletons: `is None`, `is True`, `is False`. Using `is` on numbers, strings, or lists is almost always a bug. + +## Operator precedence: what's evaluated first? -Operators are executed in the following order (from highest to lowest): +Operators are applied in this order (from highest to lowest precedence): -1. `not` (highest priority) +1. `not` (highest precedence) 2. `and` -3. `or` (lowest priority) +3. `or` (lowest precedence) ```python -# Example with priorities +# Precedence example has_ticket = True has_passport = False has_visa = True -print(f"Have ticket: {has_ticket}") -print(f"Have passport: {has_passport}") -print(f"Have visa: {has_visa}") - # Can we travel abroad? -# Need a ticket AND (passport OR visa) +# We need a ticket AND (passport OR visa) can_travel = has_ticket and (has_passport or has_visa) -print("\nCan we travel abroad?") + print(f"has_ticket and (has_passport or has_visa) = {can_travel}") + +has_ticket and (has_passport or has_visa) = True + -# Let's break down the calculation step by step: -step1 = has_passport or has_visa # First, the expression in parentheses is calculated -print(f"\nStep 1: has_passport or has_visa = {step1}") +# Step-by-step evaluation: +step1 = has_passport or has_visa # First, the expression in parentheses is evaluated +print(f"Step 1: has_passport or has_visa = {step1}") + +Step 1: has_passport or has_visa = True + step2 = has_ticket and step1 # Then the and operator is applied print(f"Step 2: has_ticket and (result of step 1) = {step2}") + +Step 2: has_ticket and (result of step 1) = True + ``` -> πŸ’‘ **Tip**: If you're unsure about the order of execution, use parentheses! -> They make the code clearer and precisely control the order of calculations. +> **Tip:** if you're unsure about the order of evaluation, use parentheses. They make the code clearer and let you control the order explicitly. -## Converting to boolean type: what counts as true? +## Conversion to boolean: what counts as true? ### The bool function -Python can convert any value to a boolean type: +Python can convert any value to a boolean: ```python print(bool(100)) + +True + print(bool(0)) + +False + print(bool("Hello")) + +True + print(bool("")) + +False + ``` -### What counts as true and false? +### What is considered true and false? -In Python, most values are considered true (`True`). +In Python most values are considered true (`True`). -Only the following are considered false (`False`): +The values considered false (`False`) are only: -- `False` (the logical "no") -- `None` (absence of value) -- Zeros: `0`, `0.0`, `0j` -- Empty containers: `""`, `()`, `[]`, `{}` +- `False` (the boolean "no") +- `None` (absence of a value) +- Zero: `0`, `0.0`, `0j` +- Empty containers: `""`, `()`, `[]`, `{}` ```python -# Examples in conditions money = 0 if money: - print("I have money!") + print("I have money") else: - print("My wallet is empty :(") # This will be printed + print("My wallet is empty") + +My wallet is empty + name = "Alex" if name: - print(f"Hello, {name}!") # This will be printed + print(f"Hello, {name}") else: - print("Hello, stranger!") + print("Hello, stranger") + +Hello, Alex + ``` ## Understanding check -Let's check how well you've understood the material: +Let's check how well you've absorbed the material: **What will the following expression return?** @@ -205,7 +265,10 @@ Let's check how well you've understood the material: result = (False or True) and not (False and True or True) ``` +1. True β€” Step by step: (False or True) = True; inside the second parens (False and True) = False, then False or True = True; not True = False; True and False = False. The correct answer is False. + +2. **Correct answer:** False β€” Step by step: (False or True) = True; inside the second parens (False and True) = False, then False or True = True; not True = False; True and False = False. + +3. None β€” Nothing here can produce None: the expression only involves True and False, so the result is one of those. Worth remembering for later though: the and/or operators return one of the values they are applied to rather than "truth in general" β€” had None been among them, it could well have come back. -Now you know how boolean data works in Python. -Although they can only take two values β€” `True` and `False`, -their importance in programming cannot be overstated. +4. Syntax error β€” This is a valid boolean expression in Python. diff --git a/course/module-1/complex-data-types/article.md b/course/module-1/complex-data-types/article.md index b69b921..8ec7d72 100644 --- a/course/module-1/complex-data-types/article.md +++ b/course/module-1/complex-data-types/article.md @@ -1,6 +1,14 @@ +--- +meta: + title: "Complex Data Types in Python" + description: "Overview of complex data types in Python: lists, tuples, sets, and dictionaries. Their features, differences, and applications." +--- + # Complex Data Types in Python -We've already familiarized ourselves with simple (scalar) data types in Python. Now it's time to move on to more powerful tools β€” complex (composite) data types, which allow us to store and process collections of values. +So far each variable has held a single value: a name, an age, a price. But data rarely comes one piece at a time. A shopping list is many items. A point on a map is latitude and longitude together. A user profile is a name, an email, and a city under one object. Making a separate variable for every value quickly becomes unworkable. + +For cases like these Python has complex types: they hold many values at once under a single name. There are four of them, and they differ in how they organize the data inside. ## What are complex data types? @@ -33,39 +41,41 @@ person = {"name": "Alex", "age": 30, "city": "New York"} Key differences between the main complex data types: -| Data Type | Syntax | Main Properties | When to Use | -| --------------------- | ------------------ | ---------------------------------------------------------------- | -------------------------------------------------------- | -| **List (list)** | `[1, 2, 3]` | βœ… Mutable
βœ… Ordered
βœ… Index access | When order matters and you need to modify the collection | -| **Tuple (tuple)** | `(1, 2, 3)` | ❌ Immutable
βœ… Ordered
βœ… Index access | To protect data from changes (coordinates, constants) | -| **Set (set)** | `{1, 2, 3}` | βœ… Mutable
❌ Unordered
❌ No indexes | When you need only unique elements | -| **Dictionary (dict)** | `{"key": "value"}` | βœ… Mutable
βœ… Ordered (since Python 3.7)
βœ… Key access | For connecting values with unique keys | +| Data Type | Syntax | Main Properties | When to Use | +| --------------------- | ------------------ | ------------------------------------------------------------------ | -------------------------------------------------------- | +| **List (list)** | `[1, 2, 3]` | βœ… Mutable
βœ… Ordered
βœ… Index access | When order matters and you need to modify the collection | +| **Tuple (tuple)** | `(1, 2, 3)` | ❌ Immutable
βœ… Ordered
βœ… Index access | To protect data from changes (coordinates, constants) | +| **Set (set)** | `{1, 2, 3}` | βœ… Mutable
❌ Unordered
❌ No indexes | When you need only unique elements | +| **Dictionary (dict)** | `{"key": "value"}` | βœ… Mutable
βœ… Ordered (since Python 3.7)
βœ… Key access | For connecting values with unique keys | + +> **Note:** an empty pair of curly braces `{}` creates an **empty dictionary**, not an empty set. To create an empty set, use `set()`. ## When to use different data types? The choice of an appropriate data type depends on the task: -- **List (list)** β€” when the order of elements is important and the collection may change +- **List (list)** β€” when the order of elements is important and the collection may change ```python # Daily to-do list todo_list = ["Buy groceries", "Walk the dog", "Write code"] ``` -- **Tuple (tuple)** β€” when data should not change and order is important +- **Tuple (tuple)** β€” when data should not change and order is important ```python # Point coordinates in 3D space point_3d = (10.5, 8.3, 9.1) ``` -- **Set (set)** β€” when you only need unique elements and order doesn't matter +- **Set (set)** β€” when you only need unique elements and order doesn't matter ```python # Unique user names unique_users = {"alice", "bob", "charlie"} ``` -- **Dictionary (dict)** β€” when data is organized as key-value pairs +- **Dictionary (dict)** β€” when data is organized as key-value pairs ```python # User information user = {"username": "alex123", "email": "alex@example.com", "active": True} @@ -96,15 +106,12 @@ Let's check how well you've understood the basic complex data types: **Which data type is best suited for storing unique elements when order doesn't matter?** +1. List (list) β€” Lists preserve the order of elements but don't guarantee their uniqueness. -## Conclusion - -Complex data types in Python are powerful tools that allow you to organize, -store, and process collections of values. +2. Tuple (tuple) β€” Tuples, like lists, preserve the order of elements but don't guarantee their uniqueness. -Choosing the right data type for a specific task can significantly simplify your code and make it more efficient. +3. **Correct answer:** Set (set) β€” Sets store only unique elements and don't preserve the order in which they were added. -In the following articles, we'll examine each of these types in detail, study their methods, and -learn how to work with them. +4. Dictionary (dict) β€” Dictionaries store key-value pairs where keys are unique, but this isn't the optimal choice if you just need to store a set of unique elements. -Let's start by exploring [lists](/guide/lists), the most flexible and frequently used complex data type in Python! +In the following lessons we'll explore each of these types in detail. Let's start with [lists](https://python-academy.org/en/guide/lists), the most flexible and frequently used complex data type. diff --git a/course/module-1/dictionaries/article.md b/course/module-1/dictionaries/article.md index 5485bfe..acdb411 100644 --- a/course/module-1/dictionaries/article.md +++ b/course/module-1/dictionaries/article.md @@ -1,299 +1,285 @@ +--- +meta: + title: "Dictionaries in Python" + description: "Dictionaries in Python: why they beat parallel lists, access via square brackets and get, adding and removing keys, iteration, and counting frequencies." +--- + # Dictionaries in Python -In this article, we'll explore dictionaries (dict) β€” one of the most powerful and flexible data types in Python. +Say you're storing students' grades. The first idea is two lists, names and grades, with matching positions linking them. -## What is a Dictionary? +```python +names = ["John", "Mary", "Kate"] +grades = [90, 75, 80] +``` -A dictionary in Python is a collection of key-value pairs. -You can think of a dictionary as a phone book, -where people's names (keys) are associated with their phone numbers (values). +To find Mary's grade you first have to locate her position in `names`, then read the same position in `grades`. Add a student and you must remember to append to the second list too. Let one list drift by a single element and Mary gets someone else's grade, and the program won't say a word. The link between a name and a grade lives only in your head, not in the code. -Main properties of dictionaries: +A dictionary removes the index middleman and ties the name straight to its value. + +```python +grades = {"John": 90, "Mary": 75, "Kate": 80} +print(grades["Mary"]) + +75 + +``` -- **Keys must be unique** β€” you cannot have two identical keys in one dictionary -- **Keys must be immutable** β€” you can use strings, numbers, tuples as keys, but not lists or dictionaries -- **Values can be of any type** β€” numbers, strings, lists, other dictionaries, etc. -- **Starting from Python 3.7, dictionaries preserve the order of adding elements** (previously order was not guaranteed) +> A dictionary (dict) is a collection of key-value pairs. You look up a value by its key, not by a numeric position. -## Creating Dictionaries +A phone book works the same way: you find a person by name, not by line number. -There are several ways to create a dictionary in Python: +## Creating a dictionary -### 1. Using curly braces {} +Most often a dictionary is written directly, in curly braces: a `key: value` pair, pairs separated by commas. ```python -# Empty dictionary -empty_dict = {} +# An empty dictionary to fill later +prices = {} -# Dictionary with data +# A dictionary with data right away person = {"name": "John", "age": 30, "city": "New York"} print(person) - - -# Nested dictionaries -nested = { - "person": {"name": "Mary", "age": 25}, - "contacts": {"email": "mary@example.com", "phone": "+1234567890"} -} -print(nested["person"]) + +{'name': 'John', 'age': 30, 'city': 'New York'} + ``` -### 2. Using the dict() constructor +If the pairs already exist somewhere, for instance arriving as pairs, you can build a dictionary with the `dict()` function. ```python -# Empty dictionary -empty_dict = dict() - -# From a list of tuples (key-value pairs) pairs = [("name", "Anna"), ("age", 28), ("city", "Berlin")] person = dict(pairs) print(person) - -# Using keyword arguments (only for string keys) -settings = dict(theme="dark", font_size=12, notifications=True) -print(settings) + +{'name': 'Anna', 'age': 28, 'city': 'Berlin'} + ``` -### 3. Dictionary comprehensions +## Reading a value: brackets vs get -```python -# Dictionary of squares (number: square of number) -squares = {x: x**2 for x in range(1, 6)} -print(squares) -``` - -### 4. The fromkeys() method for creating a dictionary with identical values +The most direct way to get a value is square brackets with the key. ```python -# Creating a dictionary with given keys and the same value for all -keys = ["name", "age", "city"] -defaults = dict.fromkeys(keys, None) -print(defaults) +person = {"name": "John", "age": 30} +print(person["name"]) + +John + ``` -## Accessing Dictionary Elements - -Access to values in a dictionary is done through keys: - -### Accessing by key +But if the key isn't in the dictionary, the brackets don't return emptiness, they stop the program with a `KeyError`. ```python -person = {"name": "John", "age": 30, "city": "New York"} - -# Getting a value by key -name = person["name"] -print(name) - -# Safe way using the get() method -phone = person.get("phone") # Returns None if the key doesn't exist -print(phone) - -# The get() method with a default value -phone = person.get("phone", "Not specified") -print(phone) +person = {"name": "John", "age": 30} +print(person["phone"]) # KeyError: 'phone' β€” there's no such key ``` -### Checking if a key exists +This isn't a flaw but a safeguard: most often reaching for a missing key is a typo or broken logic, and it's better to hear about it immediately. Brackets fit when you're sure the key exists. + +When a key might be absent, there's the `get()` method. It returns `None` instead of an error, and if you pass a second argument, that argument becomes the default value. ```python -person = {"name": "John", "age": 30, "city": "New York"} +person = {"name": "John", "age": 30} -# Checking if a key exists -print("name" in person) +print(person.get("phone")) + +None + -print("phone" not in person) +print(person.get("phone", "not provided")) + +not provided + ``` -### Accessing nested dictionaries - -```python -nested = { - "person": {"name": "Mary", "age": 25}, - "contacts": {"email": "mary@example.com", "phone": "+1234567890"} -} - -# Accessing values in nested dictionaries -name = nested["person"]["name"] -print(name) - -# Safe access using get() -email = nested.get("contacts", {}).get("email") -print(email) - -# If the path doesn't exist, None or a default value is returned -website = nested.get("contacts", {}).get("website", "Not specified") -print(website) -``` +Hence a simple rule of thumb: brackets when a missing key is an error, `get()` when a missing key is a normal case you're ready to handle. -## Modifying Dictionaries +## Adding and changing -Dictionaries in Python are mutable, so they can be easily modified: - -### Adding and changing elements +A dictionary uses the same syntax to add a key and to change an existing one: assignment by key. If the key wasn't there, it appears; if it was, the value is overwritten. ```python -# Creating a dictionary person = {"name": "John", "age": 30} -# Adding a new key and value +# Key "city" didn't exist β€” it gets added person["city"] = "New York" print(person) + +{'name': 'John', 'age': 30, 'city': 'New York'} + -# Changing the value of an existing key +# Key "age" already exists β€” the value is replaced person["age"] = 31 print(person) - -# Using the update() method for bulk updates -person.update({"age": 32, "job": "developer", "language": "Python"}) -print(person) + +{'name': 'John', 'age': 31, 'city': 'New York'} + ``` -### Removing elements +When you need several changes at once, `update()` is handier: it adds new keys and overwrites matching ones in a single call. ```python -person = {"name": "John", "age": 30, "city": "New York", "job": "developer"} - -# Removing an element by key using del -del person["job"] -print(person) - -# Removing and returning an element using pop() -age = person.pop("age") -print(age) -print(person) - -# pop() with a default value (doesn't raise an error if the key doesn't exist) -job = person.pop("job", "Not specified") -print(job) - -# Removing and returning an arbitrary element using popitem() -# In Python 3.7+ returns the last added element -item = person.popitem() -print(item) -print(person) - -# Removing all elements -person.clear() +person = {"name": "John", "age": 31} +person.update({"age": 32, "job": "developer"}) print(person) + +{'name': 'John', 'age': 32, 'job': 'developer'} + ``` -## Dictionary Methods +## Checking whether a key exists -Python provides many useful methods for working with dictionaries: - -### Getting keys, values, and key-value pairs +To find out in advance whether a key is present, use the `in` operator. This is exactly how you avoid a `KeyError` when you do need the brackets. ```python -person = {"name": "John", "age": 30, "city": "New York"} +person = {"name": "John", "age": 30} -# Getting all keys -keys = person.keys() -print(keys) +print("name" in person) + +True + + +print("phone" in person) + +False + +``` -# Getting all values -values = person.values() -print(values) +## Removing -# Getting all key-value pairs -items = person.items() -print(items) +You can drop a key with the `del` operator. But if the key isn't there, `del` also fails with a `KeyError`. -# The objects dict_keys, dict_values and dict_items are dictionary views -# They dynamically reflect changes in the dictionary -person["job"] = "developer" -print(keys) +```python +person = {"name": "John", "age": 30, "job": "developer"} -# Converting views to lists -keys_list = list(keys) -print(keys_list) +del person["job"] +print(person) + +{'name': 'John', 'age': 30} + ``` -### Copying dictionaries +The `pop()` method removes a key and returns its value at the same time, which helps when you still need the value being removed. With a second argument it doesn't fail on a missing key but returns the default instead. ```python -import copy +person = {"name": "John", "age": 30} -# Dictionary with a nested structure -original = {"name": "John", "settings": {"theme": "dark"}} +age = person.pop("age") +print(age) + +30 + -# Shallow and deep copies -shallow_copy = original.copy() -deep_copy = copy.deepcopy(original) +# Key "phone" doesn't exist, but the second argument saves us from an error +phone = person.pop("phone", "not provided") +print(phone) + +not provided + +``` -# Changing a simple key works the same in both cases -shallow_copy["name"] = "Peter" -deep_copy["name"] = "Alice" -print(f"original['name']: {original['name']}") # Remains John +## Iterating over a dictionary -# Differences appear when changing nested structures -shallow_copy["settings"]["theme"] = "light" -print(f"After shallow copy - original['settings']['theme']: {original['settings']['theme']}") # Changes! +You can walk through a dictionary with a loop. By default a `for` loop goes over the keys, and the value is easy to fetch by key. -# Create a new original to demonstrate deep copy -original = {"name": "John", "settings": {"theme": "dark"}} -deep_copy = copy.deepcopy(original) -deep_copy["settings"]["theme"] = "light" -print(f"After deep copy - original['settings']['theme']: {original['settings']['theme']}") # Doesn't change +```python +grades = {"John": 90, "Mary": 75, "Kate": 80} + +for name in grades: + print(name, ":", grades[name]) + +John : 90 +Mary : 75 +Kate : 80 + ``` -### Merging dictionaries +If you need both the key and the value inside the loop, the `items()` method hands them over as a pair at once, with no lookup by key. ```python -# Merging dictionaries with update -dict1 = {"a": 1, "b": 2} -dict2 = {"b": 3, "c": 4} +grades = {"John": 90, "Mary": 75, "Kate": 80} + +for name, grade in grades.items(): + print(name, ":", grade) + +John : 90 +Mary : 75 +Kate : 80 + +``` -# Create a copy of dict1 and update it with data from dict2 -merged = dict1.copy() -merged.update(dict2) -print(merged) +There are matching methods too: `keys()` gives only the keys, `values()` only the values. They're useful when the other half of the pair isn't needed in the loop. -# In Python 3.9+ you can use the | operator -# merged = dict1 | dict2 +```python +grades = {"John": 90, "Mary": 75, "Kate": 80} + +total = 0 +for grade in grades.values(): + total = total + grade +print("Total points:", total) + +Total points: 245 + ``` -### Setting a default value +## A common task: counting + +A dictionary is a great fit for counting something: the key is the object, the value is the counter. Let's count how many times each word appears. + +The straightforward version looks like this: for each word we check whether we've seen it before, and either bump the counter or start a new one. ```python -# Counting word frequency -counts = {} text = "one two one two three" words = text.split() -# Method 1: checking if the key exists +counts = {} for word in words: if word in counts: - counts[word] += 1 + counts[word] = counts[word] + 1 else: counts[word] = 1 print(counts) + +{'one': 2, 'two': 2, 'three': 1} + +``` + +This is exactly where `get()` with a default helps: "take the current counter, and if the word wasn't there yet, treat it as zero." The `if/else` collapses into one line. + +```python +text = "one two one two three" +words = text.split() -# Method 2: using get() counts = {} for word in words: counts[word] = counts.get(word, 0) + 1 print(counts) + +{'one': 2, 'two': 2, 'three': 1} + ``` -## Iterating Through Dictionaries +## What can be a key + +Keys live by two rules, and both follow from how a dictionary is built internally. -There are several ways to iterate through dictionary elements: +- **Keys are unique.** You can't write two identical keys: the second assignment simply overwrites the first. That makes sense β€” otherwise it would be unclear which value the key `"Mary"` should return. +- **A key must be immutable.** Strings, numbers, and tuples work. A list can't be a key: Python finds a value by the key through its immutable contents, and a list could be changed after it became a key, which would "lose" the value. ```python -person = {"name": "John", "age": 30, "city": "New York"} +# A list as a key β€” an error +broken = {[1, 2]: "value"} # TypeError: unhashable type: 'list' +``` -# Iterating through keys (default method) -print("Iterating through keys:") -for key in person: - print(key, ":", person[key]) +Values, on the other hand, can be anything: numbers, strings, lists, even other dictionaries. -# Iterating through values -print("Iterating through values:") -for value in person.values(): - print(value) +## Check your understanding -# Iterating through key-value pairs -print("Iterating through key-value pairs:") -for key, value in person.items(): - print(key, ":", value) -``` +**What does `print(person["phone"])` output if the dictionary `person` has no `"phone"` key?** + +1. It returns None and the program keeps running β€” None on a missing key is what the get() method returns. Square brackets on a non-existent key stop the program with a KeyError. + +2. **Correct answer:** The program stops with a KeyError β€” Access through brackets requires the key to exist. If it might be absent, person.get("phone") is safer β€” it returns None or a default value. + +3. A "phone" key with an empty value is added to the dictionary β€” Reading by key never adds anything to a dictionary. A new key appears only on assignment: person\["phone"] = .... diff --git a/course/module-1/functions/article.md b/course/module-1/functions/article.md index 49b1484..56a1e0a 100644 --- a/course/module-1/functions/article.md +++ b/course/module-1/functions/article.md @@ -1,19 +1,20 @@ -# Functions in Python +--- +meta: + title: "Functions in Python" + description: "How to stop copying the same code around: declaring a function with def, parameters and arguments, return, and default values." +--- -Imagine you're preparing a complex dish. Instead of coming up with a recipe from scratch each time, you can use an existing one. Functions in Python work in a similar way! 🧩 +# Functions in Python -Functions are like ready-made recipes for your code. They allow you to create blocks of code that can be used multiple times, making your programs more organized and easier to understand. +When the same block of code is needed in several places in a program, you don't have to copy it: you can describe it once as a function and then call it by name. A function takes input values, does something with them, and usually returns a result. ## What are functions? > A function is a named block of code that performs a specific task and can be called from other parts of the program. -Functions help to: +A useful way to think about a function is as a "black box": you feed in arguments, something happens inside, and a result comes back out. -- Organize code into logical blocks -- Avoid code repetition -- Make the program more readable -- Simplify debugging and maintenance +![A function as a black box: arguments 5 and 3 enter the box def add(a, b): return a + b and 8 comes out](https://python-academy.org/static/guidePage/functions/function-box-en.webp "A function as a transformation input β†’ output") ## Creating and calling functions @@ -28,28 +29,29 @@ def greet(): # Function call greet() + +Hello, world! + ``` -In this example: - -1. `def` indicates that we're creating a function -2. `greet` is the function name -3. `()` are brackets for parameters (empty for now) -4. `:` is the colon after the definition -5. Function body with indentation +Here `def` declares a function, `greet` is its name, the parentheses hold the parameters (none here), and after the colon comes the function body, indented. ### Functions with parameters -Functions can accept parameters β€” values that are passed to the function when it's called: +Functions can take data from outside. The names you write in the parentheses when declaring the function are called **parameters**, and the concrete values you pass in when calling it are **arguments**. The function below has one parameter β€” `name` β€” and we call it twice with different arguments: `"Anna"` and `"Peter"`. ```python -# Function with parameters def greet(name): print(f"Hello, {name}!") -# Function call with an argument greet("Anna") + +Hello, Anna! + greet("Peter") + +Hello, Peter! + ``` ### Return values @@ -57,81 +59,129 @@ greet("Peter") Functions can return the result of their work using the `return` operator: ```python -# Function with a return value +# A function with a return value def add(a, b): return a + b -# Using the function result +# Using the function's result result = add(5, 3) print(f"5 + 3 = {result}") + +5 + 3 = 8 + +``` + +`return` immediately exits the function: any code after it won't run. This is handy when you want to bail out early in special cases: + +```python +def describe(value): + if value < 0: + return "negative" + return "non-negative" + +print(describe(-5)) + +negative + +print(describe(10)) + +non-negative + +``` + +If a function has no `return`, it still returns a value: `None`. That's fine for functions that don't compute anything but do something (like printing to the screen): + +```python +def say_hi(): + print("Hi!") + +result = say_hi() +print(result) + +Hi! +None + ``` ## Function parameters -### Positional and named parameters +### Positional and keyword arguments Python supports two ways of passing arguments to a function: ```python -# Function with multiple parameters +# A function with multiple parameters def describe_pet(animal_type, pet_name): print(f"I have a {animal_type} named {pet_name}.") # Positional arguments describe_pet("dog", "Rex") - -# Named arguments -describe_pet(pet_name="Fluffy", animal_type="cat") + +I have a dog named Rex. + + +# Keyword arguments +describe_pet(pet_name="Whiskers", animal_type="cat") + +I have a cat named Whiskers. + ``` -### Default parameters +### Default parameter values -You can set default values for parameters: +You can specify default values for parameters: ```python -# Function with default parameters +# A function with a default parameter def describe_pet(pet_name, animal_type="dog"): print(f"I have a {animal_type} named {pet_name}.") # Using the default value describe_pet("Rex") - -# Overriding the default value -describe_pet("Fluffy", "cat") + +I have a dog named Rex. + + +# Overriding the default +describe_pet("Whiskers", "cat") + +I have a cat named Whiskers. + ``` ### Arbitrary number of arguments -Sometimes you don't know in advance how many arguments will be passed to the function. For this, special parameters are used: +Sometimes you don't know in advance how many arguments will be passed. Special parameters handle this: a star before the name (`*toppings`) gathers all positional arguments together, and two stars (`**user_info`) gather all keyword ones. ```python -# Function with an arbitrary number of positional arguments +# An arbitrary number of positional arguments def make_pizza(*toppings): - print("Making a pizza with the following toppings:") - for topping in toppings: - print(f"- {topping}") + print("Toppings:", toppings) make_pizza("pepperoni") + +Toppings: ('pepperoni',) + make_pizza("mushrooms", "green peppers", "extra cheese") - -# Function with an arbitrary number of named arguments -def build_profile(first, last, **user_info): - profile = {} - profile['first_name'] = first - profile['last_name'] = last - for key, value in user_info.items(): - profile[key] = value - return profile - -user_profile = build_profile('Anna', 'Smith', - location='New York', - field='programming') -print(user_profile) + +Toppings: ('mushrooms', 'green peppers', 'extra cheese') + + +# An arbitrary number of keyword arguments +def build_profile(**user_info): + print(user_info) + +build_profile(name="Anna", location="Moscow", field="programming") + +{'name': 'Anna', 'location': 'Moscow', 'field': 'programming'} + ``` +Python gathers all the passed values into a single structure: positional ones into a group of values, keyword ones into name-value pairs. What these structures are and how to go through them one by one, we'll cover in the chapters on collections and loops. + ## Understanding check -Let's check how well you've understood the topic of functions: +Let's check how well you've absorbed the topic of functions: **What will the following code output?** @@ -142,3 +192,27 @@ def mystery(x, y=2): result = mystery(3) print(result) ``` + +1. 3 β€” The function multiplies x by y, where y defaults to 2. + +2. **Correct answer:** 6 β€” The function multiplies 3 by 2 (the default value for y). + +3. Error β€” The code runs without errors, since y has a default value. + +4. 9 β€” To get 9 you'd need to pass 3 and 3 as arguments. + +## Practice tasks + +Right below this article there's a block called **Practice tasks**. This is the first time you write a whole function: the `def` line with the name and parameters is already in the editor, and you fill in the body. + +### Where the values come from + +The values the function works with arrive in its parameters. You don't need to invent or enter them: each test calls the function with its own values. + +![Anatomy of a test: its own parameter values, the function call and the expected result](https://python-academy.org/static/guidePage/functions/exercise-test-case-en.webp "Anatomy of a test case") + +A test tab shows everything at once: which values it passes in, how it calls the function, and what result it expects. Every test uses its own values, and all of them need to pass. + +### The result must be returned + +The function must **return** its result with `return`. If you print it with `print()` instead, the test won't pass: the check looks at the returned value, not at what appeared on screen. diff --git a/course/module-1/if-statement/article.md b/course/module-1/if-statement/article.md index 1d5063b..1fd1e81 100644 --- a/course/module-1/if-statement/article.md +++ b/course/module-1/if-statement/article.md @@ -1,97 +1,108 @@ -# Conditional Constructs. If Statement +--- +meta: + title: "Conditional construct. The if statement" + description: "Detailed explanation of conditional constructs in Python: syntax, usage options, nested conditions, and practical examples." +--- -In this article, we'll get acquainted with conditional constructs in Python β€” one -of the most important tools in programming. Without them, our programs would be like a train -that can only move straight ahead, without the ability to choose a path at a fork. +# Conditional construct. The if statement -Conditional constructs allow a program to "make decisions" and choose different actions depending on circumstances. +A program doesn't always run in a straight line. Whether to let a user onto an "18+" page, show a discount, save a file or warn that it already exists β€” at each such fork you need to check a condition and take one path or another depending on the answer. -## What is a conditional construct? +In Python you create a fork with the `if` statement: it checks a condition and runs a block of code only if the condition is true. -A conditional construct is a tool that allows different blocks of code to be executed -depending on whether a certain condition is true (`True`) or false (`False`). +## The basic conditional construct -In Python, the main conditional construct is the `if` operator. -It checks if a condition is true, and if it is β€” executes the corresponding block of code. +### Syntax of the if statement -## Basic conditional operator constructs - -### If operator syntax - -The basic syntax of the if conditional construct in Python looks like this: +The basic syntax of the `if` conditional construct in Python looks like this: ```python -# Simple example of the if operator +# A simple if example age = 18 if age >= 18: - print("You are an adult!") - -print("This line will always execute, regardless of the condition.") + print("You are an adult.") + +You are an adult. + + +print("This line runs no matter the condition.") + +This line runs no matter the condition. + ``` -Note several important details: +A few important details: -1. A colon `:` is placed after the condition -2. The code block that should execute when the condition is true **must** have an indentation (usually 4 spaces or one tab) -3. Once the indentation ends, it marks the end of the if block +1. The condition is followed by a colon `:` +2. The block of code to execute when the condition is true **must** be indented (typically 4 spaces or one tab) +3. As soon as the indentation ends, the `if` block is over -Let's change the age value and look at the result: +Let's change the age and see what happens: ```python age = 16 if age >= 18: - print("You are an adult!") + print("You are an adult.") -print("This line will always execute, regardless of the condition.") +print("This line runs no matter the condition.") + +This line runs no matter the condition. + ``` -In this case, the condition `age >= 18` is not true (it's false), so the code block inside `if` doesn't execute, and we only see the second line. +Here the condition `age >= 18` is not true (it's false), so the block inside `if` doesn't run, and we see only the second line. -### Using else +### Using else (otherwise) -Often we need to execute one block of code if the condition is true, and another if the condition is false. For this, the `if-else` construct is used: +Often we want one block to run if the condition is true, and another if it's false. That's what `if-else` is for: ```python temperature = 15 if temperature > 20: - print("It's warm today, you can wear a t-shirt!") + print("It's warm, a t-shirt will do.") else: - print("It's cool today, take a jacket!") + print("It's chilly, grab a jacket.") + +It's chilly, grab a jacket. + ``` -Here, if the temperature is above 20, the first block will execute. Otherwise, the block after `else` executes. In our example, the temperature is 15, which is less than 20, so the code in the `else` block executes. +If the temperature is above 20, the first block runs. Otherwise the block after `else` runs. In our example the temperature is 15, less than 20, so the `else` block runs. -### The elif construct β€” "else if" +### The elif clause (else if) -What if we have more than two choice options? For example, we want different messages for different temperature ranges. For this, the `elif` construct is used (short for "else if"): +What if there are more than two cases to handle? For example, different messages for different temperature ranges. That's where `elif` (short for "else if") comes in: ```python temperature = 25 if temperature < 0: - print("Very cold! Wear a warm jacket and hat!") + print("Very cold. Put on a warm jacket and a hat.") elif temperature < 10: print("Cold. Dress warmly.") elif temperature < 20: - print("Cool. A light jacket wouldn't hurt.") + print("Chilly. A light jacket wouldn't hurt.") elif temperature < 30: - print("Warm. T-shirt and shorts will do.") + print("Warm. A t-shirt and shorts will do.") else: - print("Hot! Bring water with you!") + print("Hot. Bring some water.") + +Warm. A t-shirt and shorts will do. + ``` -In this example, Python checks each condition in order. As soon as it finds the first true condition, it executes the corresponding code block, and all subsequent conditions are skipped. If none of the conditions are true, the `else` block executes (if present). +Python checks each condition in order. As soon as it finds the first true one, the corresponding block runs and all the remaining conditions are skipped. If none of them are true, the `else` block runs (if it's present). -> **Important**: Python checks conditions from top to bottom and stops as soon as it finds the first true condition. The order of conditions matters! +> **Important:** Python checks conditions top to bottom and stops at the first one that's true. The order of conditions matters. -## Advanced features of conditional constructs +## More advanced conditionals ### Nested conditions -Conditional constructs can be nested inside each other, creating more complex logic: +Conditional constructs can be nested inside one another to express more complex logic: ```python age = 25 @@ -103,22 +114,26 @@ if age >= 18: if has_license: print("You can drive a car.") else: - print("To drive a car, you need to get a license.") + print("To drive a car you need to get a license.") else: print("You are a minor and cannot drive a car.") + +You are an adult. +You can drive a car. + ``` -However, don't overuse nested conditions β€” code with many levels of nesting becomes difficult to read. Often such logic can be rewritten using logical operators. +Don't overuse nested conditions, though: code with many levels of nesting becomes hard to read. Often the same logic can be rewritten using logical operators. ### Logical operators in conditions -Logical operators are used to create complex conditions: +To build more complex conditions, use logical operators: -- `and` β€” logical AND (condition is true if both expressions are true) -- `or` β€” logical OR (condition is true if at least one expression is true) -- `not` β€” logical NOT (inverts the value of the expression) +- `and` (logical AND): the condition is true if both expressions are true +- `or` (logical OR): the condition is true if at least one expression is true +- `not` (logical NOT): inverts the value of the expression -Let's rewrite our example with nested conditions: +Let's rewrite the nested-condition example: ```python age = 25 @@ -127,84 +142,200 @@ has_license = True if age >= 18 and has_license: print("You are an adult and can drive a car.") elif age >= 18 and not has_license: - print("You are an adult, but to drive a car you need to get a license.") + print("You are an adult, but you need a license to drive.") else: print("You are a minor and cannot drive a car.") + +You are an adult and can drive a car. + ``` -Such code is more readable and easier to maintain. - -Let's look at another example with the `or` operator: +Another example with the `or` operator: ```python day = "Saturday" if day == "Saturday" or day == "Sunday": - print("It's a weekend! You can relax. 😎") + print("It's the weekend, time to rest.") else: - print("It's a workday. Need to go to work. πŸ’Ό") + print("It's a workday, off to work.") + +It's the weekend, time to rest. + ``` -### Ternary operator +### The ternary operator -For simple conditions, you can use the ternary operator β€” a compact form of writing a condition: +For simple conditions you can use the ternary operator β€” a compact form of `if-else`: ```python age = 20 -# Regular notation using if-else +# Regular if-else form if age >= 18: status = "an adult" else: status = "a minor" -print(f"Regular notation: You are {status}") +print(f"Regular form: You are {status}") + +Regular form: You are an adult + -# The same logic, but using the ternary operator +# The same logic via the ternary operator status = "an adult" if age >= 18 else "a minor" -print(f"Ternary operator: You are {status}") +print(f"Ternary: You are {status}") + +Ternary: You are an adult + ``` -The syntax of the ternary operator: +Ternary syntax: ```python value_if_true if condition else value_if_false ``` -The ternary operator makes the code more compact, but don't abuse it β€” complex conditions are better written in the regular format for readability. +Don't overuse it: complex conditions are easier to read in regular `if-else` form. + +## Pythonic style for conditions + +Python has a few idiomatic patterns for conditions that make code shorter and more readable. Get used to them early. + +### Truthy checks + +Beginners often write conditions with explicit comparisons to `True`, `False`, `0`, an empty string: + +```python +if is_active == True: +if my_list == []: +if name == "": +``` + +From the boolean lesson we know that any value used in a condition is treated as `True` or `False` on its own. So these checks can be simplified: + +```python +if is_active: # already bool +if not my_list: # empty list is falsy +if not name: # empty string is falsy +``` + +Shorter and easier on the eye of any Python programmer. + +### Chained comparisons + +When you need to check whether a value is in a range, most languages require: + +```python +if x >= 0 and x < 100: +``` + +Python lets you write the same as a single chain, just like in math: + +```python +if 0 <= x < 100: +``` + +Same behaviour, reads more naturally. Especially handy with `elif` for ranges: + +```python +if temperature < 0: + ... +elif 0 <= temperature < 10: + ... +elif 10 <= temperature < 20: + ... +``` + +Now each branch is self-documenting: you can see exactly which range it handles, without having to remember what the previous branch already filtered out. + +### The in operator for membership + +When you need to check whether a value matches one of several options, you might write it with `or`: + +```python +if day == "Saturday" or day == "Sunday": + ... +``` + +The Pythonic version with `in` is shorter and more readable: -## Practical examples of using conditions +```python +if day in ("Saturday", "Sunday"): + ... +``` -Conditional constructs are widely used in everyday programming. Let's look at several practical examples: +The bigger the option set, the bigger the win: adding a third day is one tiny edit instead of another `or`. -### Checking if a number is even +## Practical examples -One of the simple and frequently used applications of conditions is determining if a number is even: +### Checking whether a number is even + +A common use of conditions is checking the parity of a number: ```python number = 42 if number % 2 == 0: - print(f"{number} is an even number") + print(f"{number} is even") else: - print(f"{number} is an odd number") + print(f"{number} is odd") + +42 is even + ``` ### Password verification -Conditions help implement data verification against certain criteria: +Conditions help validate data against rules. Here `.isalpha()` returns `True` if the string consists only of letters, and `.isdigit()` returns `True` if it consists only of digits: ```python password = "password123" if len(password) < 8: - print("Password is too short!") + print("Password is too short.") elif password.isalpha(): - print("Password must contain not only letters!") + print("Password must contain more than just letters.") elif password.isdigit(): - print("Password must contain not only digits!") + print("Password must contain more than just digits.") else: - print("Password is suitable!") + print("Password is acceptable.") + +Password is acceptable. + +``` + +## Common beginner mistakes + +### Confusing = and == + +In a condition you want `==` (comparison), not `=` (assignment). They are two different things: + +```python +if x == 5: # correct: checks whether x equals five + ... + +if x = 5: # SyntaxError: you can't assign inside a condition + ... +``` + +### Missing colon + +After an `if`, `elif`, or `else` condition the `:` is required: + +```python +if x > 5 # SyntaxError: missing colon + print("greater than five") +``` + +### Inconsistent indentation + +The body of a condition must be indented consistently. Changing the number of spaces inside one block, or mixing tabs and spaces, is not allowed: + +```python +if x > 5: + print("a") + print("b") # IndentationError: extra indentation ``` ## Understanding check @@ -224,3 +355,11 @@ elif x == 5: else: print("D") ``` + +1. A β€” The condition x > 10 fails since x is 5. + +2. B β€” The condition x > 5 fails since x is 5. + +3. **Correct answer:** C β€” The condition x == 5 is true since x is indeed 5. + +4. D β€” The else block runs only when all previous conditions are false, but x == 5 is true. diff --git a/course/module-1/lists/article.md b/course/module-1/lists/article.md index f4cb858..7589153 100644 --- a/course/module-1/lists/article.md +++ b/course/module-1/lists/article.md @@ -1,40 +1,64 @@ +--- +meta: + title: "Lists in Python" + description: "Detailed description of lists in Python: creation, methods for working with lists, operations, and practical usage examples." +--- + # Lists in Python -In this article, we will take a detailed look at lists in Python β€” one of the most flexible and frequently used data types. Lists allow you to store collections of elements and manipulate them in various ways. +Say you're keeping a shopping list. You could make a separate variable for each item: `item1`, `item2`, `item3`. But that turns into a mess fast: you can't insert an item in the middle, you can't count how many items there are, and you can't go through them all in turn. You need one variable that holds many values in order. That's a list. -## What is a list? +```python +shopping = ["bread", "milk", "eggs"] +print(shopping) + +['bread', 'milk', 'eggs'] + +``` -A list in Python is an ordered, mutable collection of elements that can be of different types. You can think of a list as a container that stores various objects arranged in a specific order. +> A list (list) is an ordered, mutable collection of elements. Under a single name lie several values at once, each has its own position, and the set can be changed: add, remove, rearrange. -The main properties of lists: +From this definition follow four properties we'll rely on going forward: -- **Ordered**: elements are stored in the order they were added -- **Mutable**: you can add, remove, and change elements after creating the list -- **Indexable**: each element can be accessed by its position (index) -- **Allow duplicates**: the same element can appear in the list multiple times +- **Order is preserved** β€” elements stay in the order you added them +- **A list is mutable** β€” after creation you can add and remove elements +- **Access by index** β€” any element can be reached by its position +- **Duplicates are allowed** β€” the same value can appear multiple times ## Creating lists There are several ways to create lists in Python: -### Using square brackets [] +### Using square brackets \[] ```python # Empty list empty_list = [] print(empty_list) + +[] + # List of numbers numbers = [1, 2, 3, 4, 5] print(numbers) + +[1, 2, 3, 4, 5] + # List of different data types mixed = [1, "hello", True, 3.14] print(mixed) + +[1, 'hello', True, 3.14] + # Nested lists (list of lists) matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] print(matrix) + +[[1, 2, 3], [4, 5, 6], [7, 8, 9]] + ``` ### Using the list() constructor @@ -43,29 +67,23 @@ print(matrix) # Creating an empty list empty_list = list() print(empty_list) + +[] + # Creating a list from a string (each character becomes an element) chars = list("Python") print(chars) + +['P', 'y', 't', 'h', 'o', 'n'] + # Creating a list from other iterable objects tuple_to_list = list((1, 2, 3)) print(tuple_to_list) - -set_to_list = list({1, 2, 3}) -print(set_to_list) -``` - -### Using list comprehensions - -```python -# Creating a list of squares of numbers from 0 to 9 -squares = [x**2 for x in range(10)] -print(squares) - -# Creating a list of even numbers from 0 to 9 -even_numbers = [x for x in range(10) if x % 2 == 0] -print(even_numbers) + +[1, 2, 3] + ``` ## Accessing list elements @@ -80,13 +98,22 @@ fruits = ["apple", "banana", "cherry", "pear", "orange"] # Getting elements by index first_fruit = fruits[0] print(f"First fruit: {first_fruit}") + +First fruit: apple + # Negative indices for accessing from the end of the list last_fruit = fruits[-1] print(f"Last fruit: {last_fruit}") + +Last fruit: orange + second_last = fruits[-2] print(f"Second to last fruit: {second_last}") + +Second to last fruit: pear + ``` ### Slices @@ -102,23 +129,75 @@ fruits = ["apple", "banana", "cherry", "pear", "orange"] # First three elements first_three = fruits[0:3] print(f"First three fruits: {first_three}") + +First three fruits: ['apple', 'banana', 'cherry'] + # Same thing, but the starting index can be omitted if it's 0 first_three = fruits[:3] print(f"First three fruits: {first_three}") + +First three fruits: ['apple', 'banana', 'cherry'] + # Every second element every_second = fruits[::2] print(f"Every second fruit: {every_second}") + +Every second fruit: ['apple', 'cherry', 'orange'] + # Reversing the list reversed_list = fruits[::-1] print(f"List in reverse order: {reversed_list}") + +List in reverse order: ['orange', 'pear', 'cherry', 'banana', 'apple'] + +``` + +## Looping over a list + +Reaching for elements by index is handy when you need one or two. But to go through them all, there's a shorter way β€” the `for` loop you already know: + +```python +fruits = ["apple", "banana", "cherry"] + +for fruit in fruits: + print(f"Fruit: {fruit}") + +Fruit: apple +Fruit: banana +Fruit: cherry + +``` + +To ask how many elements a list holds, use the `len()` function: + +```python +fruits = ["apple", "banana", "cherry"] +print(f"Fruits in total: {len(fruits)}") + +Fruits in total: 3 + +``` + +Sometimes you need the position along with the element. No need for a separate counter variable β€” `enumerate()` hands you the index and the value as a pair: + +```python +fruits = ["apple", "banana", "cherry"] + +for index, fruit in enumerate(fruits): + print(f"Index {index}: {fruit}") + +Index 0: apple +Index 1: banana +Index 2: cherry + ``` ## Modifying list elements -Unlike strings, lists are mutable, meaning you can change, add, and remove elements: +The key difference from strings: **lists are mutable**. A string can't be changed in place β€” you have to build a new one (recall the strings lesson). But list elements can be changed, added, and removed inside the existing object: ```python fruits = ["apple", "banana", "cherry"] @@ -126,16 +205,25 @@ fruits = ["apple", "banana", "cherry"] # Changing an element fruits[0] = "kiwi" print(fruits) + +['kiwi', 'banana', 'cherry'] + # Changing multiple elements using a slice numbers = [1, 2, 3, 4, 5] numbers[1:4] = [20, 30, 40] print(numbers) + +[1, 20, 30, 40, 5] + # You can even replace multiple elements with a different number of elements numbers = [1, 2, 3, 4, 5] numbers[1:4] = [20, 30] print(numbers) + +[1, 20, 30, 5] + ``` ## Main list methods @@ -150,19 +238,31 @@ fruits = ["apple", "banana"] # Adding an element to the end of the list fruits.append("cherry") print(fruits) + +['apple', 'banana', 'cherry'] + # Inserting an element at a specific position fruits.insert(1, "orange") print(fruits) + +['apple', 'orange', 'banana', 'cherry'] + # Adding elements from another list more_fruits = ["pear", "grape"] fruits.extend(more_fruits) print(fruits) + +['apple', 'orange', 'banana', 'cherry', 'pear', 'grape'] + # Combining lists using the + operator combined = fruits + ["pineapple", "mango"] print(combined) + +['apple', 'orange', 'banana', 'cherry', 'pear', 'grape', 'pineapple', 'mango'] + ``` ### Removing elements @@ -173,28 +273,52 @@ fruits = ["apple", "banana", "cherry", "orange", "banana"] # Removing an element by value (removes only the first occurrence) fruits.remove("banana") print(fruits) + +['apple', 'cherry', 'orange', 'banana'] + # Removing an element by index and returning its value removed = fruits.pop(1) print(f"Removed: {removed}") + +Removed: cherry + print(f"List after removal: {fruits}") + +List after removal: ['apple', 'orange', 'banana'] + # If no index is specified, pop() removes and returns the last element last = fruits.pop() print(f"Last element: {last}") + +Last element: banana + print(fruits) + +['apple', 'orange'] + # Removing all elements from the list fruits.clear() print(f"Empty list: {fruits}") + +Empty list: [] + # The del operator for removing elements by index or slice numbers = [1, 2, 3, 4, 5] del numbers[0] print(numbers) + +[2, 3, 4, 5] + del numbers[1:3] print(numbers) + +[2, 5] + ``` ### Finding and counting elements @@ -204,15 +328,27 @@ fruits = ["apple", "banana", "cherry", "banana", "pear"] # Checking if an element is in the list print("banana" in fruits) + +True + print("watermelon" in fruits) + +False + # Finding the index of the first occurrence of an element banana_index = fruits.index("banana") print(f"Index of the first banana: {banana_index}") + +Index of the first banana: 1 + # Counting the number of occurrences of an element banana_count = fruits.count("banana") print(f"Number of bananas: {banana_count}") + +Number of bananas: 2 + ``` ### Sorting and reversing @@ -222,21 +358,35 @@ print(f"Number of bananas: {banana_count}") numbers = [3, 1, 4, 1, 5, 9, 2] numbers.sort() print(f"Sorted list: {numbers}") + +Sorted list: [1, 1, 2, 3, 4, 5, 9] + # Sorting in reverse order numbers.sort(reverse=True) print(f"Reverse sort: {numbers}") + +Reverse sort: [9, 5, 4, 3, 2, 1, 1] + # If you don't want to modify the original list, use sorted() original = [3, 1, 4, 1, 5] sorted_list = sorted(original) print(f"Original: {original}") + +Original: [3, 1, 4, 1, 5] + print(f"Sorted copy: {sorted_list}") + +Sorted copy: [1, 1, 3, 4, 5] + ``` ## Copying lists -When working with lists, it's important to understand how assignment and copying work: +This is one of the most common sources of bugs for beginners: assigning a list **does not create a copy**. Both variables point to the same object in memory. If you change the list through one variable, the other will see the change. + +![Assignment vs copying: with reference = original both variables point to the same box, while copy = original.copy() creates a new box](https://python-academy.org/static/guidePage/lists/copy-vs-reference-en.webp "Assignment vs copying") ```python # Create a list @@ -246,6 +396,9 @@ original = [1, 2, 3] reference = original reference.append(4) print(f"Original after changing the reference: {original}") + +Original after changing the reference: [1, 2, 3, 4] + # Correct ways to copy a list: # 1. The copy() method @@ -260,9 +413,51 @@ copy3 = list(original) # Let's check that the copies are not linked to the original copy1.append(5) print(f"Original: {original}") + +Original: [1, 2, 3, 4] + print(f"Copy 1: {copy1}") + +Copy 1: [1, 2, 3, 4, 5] + +``` + +## List comprehensions + +A very common task: take one list and build another from it. Done plainly, that's a few lines β€” set up an empty list, walk it with a loop, append one by one: + +```python +squares = [] +for i in range(10): + squares.append(i**2) +print(squares) + +[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + +``` + +Common enough that Python has a shorthand for it β€” the **list comprehension**. Same result, one line: + +```python +squares = [i**2 for i in range(10)] +print(squares) + +[0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + +``` + +Read it not left to right but starting from the `for`: `for i in range(10)` is where the values come from, `i**2` is what goes into the new list. You can add a condition at the end, and then only the matching values make it through: + +```python +even_squares = [i**2 for i in range(10) if i % 2 == 0] +print(even_squares) + +[0, 4, 16, 36, 64] + ``` +The temptation to cram everything into one line is strong, but a comprehension is good exactly as long as it reads at a glance. Once two conditions and a nested loop want in, a plain `for` will be clearer β€” both for you and for whoever opens the code six months from now. + ## Practical example of using a list ```python @@ -274,8 +469,19 @@ grades = [95, 82, 90, 78, 88] highest_score = max(grades) top_student_index = grades.index(highest_score) print(f"Best student: {students[top_student_index]} with a grade of {highest_score}") + +Best student: Anna with a grade of 95 + ``` ## Understanding check **Which of the following methods will correctly create a copy of a list that is not linked to the original?** + +1. copy_list = original_list β€” This does not create a copy, but merely gives another name to the same list. Changes in copy_list will be reflected in original_list. + +2. **Correct answer:** copy_list = original_list.copy() β€” The .copy() method creates a new copy of the list. Changes to the copy will not affect the original (however, be careful with nested objects - this is a shallow copy). + +3. **Correct answer:** copy_list = original_list\[:] β€” Using the slice \[:] creates a new copy of the entire list. This is one of the correct ways to create a copy in Python. + +4. **Correct answer:** copy_list = list(original_list) β€” The list() function creates a new list based on an existing one, which is a proper way to create a copy. diff --git a/course/module-1/loops/article.md b/course/module-1/loops/article.md index 5b07a32..f09e54b 100644 --- a/course/module-1/loops/article.md +++ b/course/module-1/loops/article.md @@ -1,39 +1,39 @@ -# Iterations and Loops - -Imagine that you need to repeat some action many times. For example, greeting each guest at a party or counting all items in a list. Doing this manually would be tedious. And this is where loops come to the rescue! πŸ”„ +--- +meta: + title: "Iterations and Loops" + description: "Loops in Python: for and while, iterating over strings and number ranges, break and continue, nested loops and practical examples." +--- -Loops help automate repetitive actions, allowing you to execute the same block of code multiple times. This saves time and makes your code cleaner and more efficient. - -## What are iterations and loops? +# Iterations and Loops -> An iteration is a single execution of a set of instructions. And a loop is a construct that allows a block of code to be executed multiple times while a certain condition is met. +You need to print a word letter by letter β€” each on its own line. Without a loop that's six separate `print` calls: one for "P", one for "y", one for "t", and so on. A longer word means more lines by hand, and if the word only becomes known while the program runs, you can't write them out in advance. -Python offers two main types of loops: +A loop spares you that: you describe the step once, and Python repeats it for each letter on its own. One such repetition is called an **iteration**. -- The `for` loop: used to iterate over a sequence (list, tuple, string, etc.) -- The `while` loop: executes as long as the specified condition remains true +Python has two loops: `for` walks through a ready-made set of values, `while` keeps going as long as a condition holds. Let's start with the first. ## The for loop -The `for` loop is used to iterate through elements of a sequence (list, tuple, string, etc.). This is the most common type of loop in Python. - -### For loop syntax - -The basic syntax of a for loop looks like this: +A string is a sequence of characters, and `for` can walk straight through it: ```python -# Simple example of a for loop -fruits = ["apple", "banana", "cherry"] +message = "Python" -for fruit in fruits: - print(f"I love {fruit}!") +for char in message: + print(char) + +P +y +t +h +o +n + ``` -In this example: +It reads almost like English: "for each character `char` in the string `message` β€” print it". Python takes the values one by one, binds each to the name `char`, and runs the indented body. When the values run out, the loop ends β€” you don't have to track that yourself. -1. `fruit` is the iterator variable that takes the value of each element in the `fruits` list in turn -2. The loop body executes for each element in the list -3. After `:` follows an indented code block that is executed in each iteration +`for` walks any set of values the same way, not just strings: lists, tuples and dictionaries are waiting in the chapters ahead, and you'll walk them with this very loop. ### Iterating over a range of numbers @@ -43,9 +43,18 @@ Often we need to execute a loop a certain number of times. For this, the `range( # Using range() to create a sequence of numbers for i in range(5): # from 0 to 4 print(f"Number: {i}") + +Number: 0 +Number: 1 +Number: 2 +Number: 3 +Number: 4 + ``` -The `range()` function creates a sequence of numbers and has several usage options: +The `range()` function creates a sequence of numbers and has several usage options. + +The examples below use `print(..., end=" ")`: by default `print` appends a newline at the end, but the `end=" "` parameter replaces it with a space so values print on a single line. ```python # Range() usage options @@ -54,39 +63,24 @@ The `range()` function creates a sequence of numbers and has several usage optio for i in range(3): print(i, end=" ") print() # For line break + +0 1 2 + # range(start, stop): from start to stop-1 for i in range(2, 5): print(i, end=" ") print() + +2 3 4 + # range(start, stop, step): from start to stop-1 with step for i in range(1, 10, 2): # Odd numbers from 1 to 9 print(i, end=" ") -``` - -### Iterating over strings - -Strings in Python are also sequences, so we can iterate through their characters: - -```python -# Iterating through characters in a string -message = "Python" - -for char in message: - print(char) -``` - -### Iterating with indices - -Sometimes we need both elements and their indices. For this, the `enumerate()` function is used: - -```python -# Getting indices and values -fruits = ["apple", "banana", "cherry"] - -for index, fruit in enumerate(fruits): - print(f"Index {index}: {fruit}") + +1 3 5 7 9 + ``` ## The while loop @@ -102,15 +96,16 @@ count = 1 while count <= 5: print(f"Loop is executing for the {count}th time") count += 1 # Increasing the counter + +Loop is executing for the 1th time +Loop is executing for the 2th time +Loop is executing for the 3th time +Loop is executing for the 4th time +Loop is executing for the 5th time + ``` -In this example: - -1. The loop executes while `count <= 5` -2. Inside the loop, we increase `count` by 1 in each iteration -3. When `count` becomes 6, the condition becomes false, and the loop terminates - -> **Important**: In a `while` loop, you always need to change the variable involved in the condition, otherwise the loop may become infinite! +The loop runs while the condition `count <= 5` is true. Inside, you must change the variable from the condition (here `count += 1`), otherwise the loop will be infinite. ### Infinite loop @@ -127,16 +122,26 @@ while True: # This condition is always true if counter >= 5: # Condition to exit the loop print("Exiting the loop!") break # Interrupts the loop execution + +Iteration 0 +Iteration 1 +Iteration 2 +Iteration 3 +Iteration 4 +Exiting the loop! + ``` ## Loop control -Python provides several tools for controlling loop execution. +Python provides several tools for controlling loop execution. The two main ones are `break` and `continue`. ### The break statement The `break` statement allows you to immediately exit a loop, regardless of the condition: +![The loop walks through numbers 0–9, hits break at 5 and exits the loop; numbers 6–9 are not visited](https://python-academy.org/static/guidePage/loops/break-en.webp "break β€” exit the loop") + ```python # Using break to exit a loop for i in range(10): @@ -144,12 +149,18 @@ for i in range(10): if i == 5: print("\nReached number 5, exiting the loop!") break + +0 1 2 3 4 5 +Reached number 5, exiting the loop! + ``` ### The continue statement The `continue` statement skips the current iteration and moves to the next one: +![The loop walks through numbers 0–5, hits continue at 3, skips over it, and resumes at 4](https://python-academy.org/static/guidePage/loops/continue-en.webp "continue β€” skip iteration") + ```python # Using continue to skip iterations for i in range(10): @@ -157,28 +168,28 @@ for i in range(10): if i % 2 == 0: continue print(i, end=" ") + +1 3 5 7 9 + ``` ### The else block in loops -`for` and `while` loops can have an `else` block that executes when the loop terminates normally (not through `break`): +`for` and `while` loops can have an `else` block that runs when the loop ends normally (not via `break`): ```python -# Using else with a loop for i in range(5): print(i, end=" ") else: - print("\nLoop completed normally") - -# Example with break - the else block won't execute -for i in range(5): - print(i, end=" ") - if i == 2: - break -else: - print("\nThis line won't be output because the loop was interrupted") + print("\nLoop completed without break") + +0 1 2 3 4 +Loop completed without break + ``` +The feature exists, but it's rarely used in real-world code. + ## Nested loops Loops can be nested inside each other to process multidimensional data structures or solve more complex problems: @@ -189,35 +200,15 @@ for i in range(1, 4): # Rows for j in range(1, 4): # Columns print(f"{i} Γ— {j} = {i*j}", end="\t") print() # Moving to a new line after each row of the table + +1 Γ— 1 = 1 1 Γ— 2 = 2 1 Γ— 3 = 3 +2 Γ— 1 = 2 2 Γ— 2 = 4 2 Γ— 3 = 6 +3 Γ— 1 = 3 3 Γ— 2 = 6 3 Γ— 3 = 9 + ``` > **Tip**: Nested loops can be resource-intensive, especially with a large number of iterations. Use them carefully and look for more efficient alternatives for processing large volumes of data. -## List Comprehensions - -List comprehensions are an elegant way to create lists in one line using syntax similar to a `for` loop: - -```python -# Creating a list of squares of numbers from 0 to 9 -# Regular way -squares = [] -for i in range(10): - squares.append(i**2) -print(f"Regular way: {squares}") - -# Using list comprehensions -squares_comprehension = [i**2 for i in range(10)] -print(f"Using list comprehension: {squares_comprehension}") -``` - -List comprehensions can also contain conditions: - -```python -# Filtering with a condition: only even numbers -even_squares = [i**2 for i in range(10) if i % 2 == 0] -print(f"Squares of even numbers: {even_squares}") -``` - ## Understanding check Let's check how well you've understood the topic of loops: @@ -231,3 +222,11 @@ for i in range(1, 5): total += i print(total) ``` + +1. 0 β€” In the loop we sum all even numbers from 1 to 4, which are 2 and 4. + +2. **Correct answer:** 6 β€” We sum the even numbers from 1 to 4, which are 2 + 4 = 6. + +3. 10 β€” The number 10 would be the result of summing all numbers from 1 to 4 (1 + 2 + 3 + 4), but in this code only even numbers are summed. + +4. 3 β€” The number 3 would be the result of summing 1 and 2, but in this code only even numbers from 1 to 4 are summed, namely 2 and 4. diff --git a/course/module-1/numeric-data-types/article.md b/course/module-1/numeric-data-types/article.md index c62c02a..770e441 100644 --- a/course/module-1/numeric-data-types/article.md +++ b/course/module-1/numeric-data-types/article.md @@ -1,14 +1,21 @@ -# Numeric Data Types in Python +--- +meta: + title: "Numeric data types in Python" + description: "Integers (int) and floating-point numbers (float) in Python, arithmetic operations, rounding, and the subtleties of working with fractions." +--- -Python provides various types for working with numbers, making it a powerful tool for mathematical calculations. In this article, we'll examine in detail the numeric data types and operations that can be performed with them. +# Numeric data types in Python -## Basic Numeric Types +Python provides several types for working with numbers, which makes it a powerful tool for mathematical computations. In this article we'll go through the numeric data types and the operations you can perform on them. -Python has three main numeric types: +## The main numeric types -1. **Integers** (int) β€” numbers without a fractional part -2. **Floating-point numbers** (float) β€” numbers with a fractional part -3. **Complex numbers** (complex) β€” numbers with real and imaginary parts +Python has two main numeric types: + +- **Integers** (int): numbers without a fractional part +- **Floating-point numbers** (float): numbers with a fractional part + +There's also `complex` for imaginary numbers, but you'll almost never encounter it in regular development. ### Integers (int) @@ -20,199 +27,169 @@ positive = 42 negative = -73 zero = 0 -# Large numbers (Python has no limit on the size of integers) big_number = 1234567890123456789012345678901234567890 -# Binary, octal, and hexadecimal numbers -binary = 0b1010 # 10 in decimal -octal = 0o756 # 494 in decimal -hexadecimal = 0xFF # 255 in decimal - -# Using separator for easier reading of large numbers -million = 1_000_000 # This is the same as 1000000 +# Underscore separator for readability of large numbers +million = 1_000_000 # The same as 1000000 ``` -Unlike many other programming languages, Python integers have unlimited precision, meaning they can be arbitrarily large (limited only by the available computer memory). +Unlike many other languages, Python integers have unlimited precision: they can be arbitrarily large, limited only by available memory. ### Floating-point numbers (float) Floating-point numbers (type `float`) represent real numbers with a fractional part: ```python -# Numbers with a decimal point price = 19.99 pi_approx = 3.14159 negative_float = -0.5 - -# Exponential notation (scientific notation) -avogadro = 6.022e23 # 6.022 * 10^23 -tiny = 1.6e-35 # 1.6 * 10^(-35) ``` -Floating-point numbers have limited precision and can only represent approximate values of some decimal fractions. This can lead to small inaccuracies in calculations: +The key thing to know about floats: their precision is limited, and seemingly simple calculations can produce small errors: ```python -# Example of limited float precision result = 0.1 + 0.2 -print(result) # Will output 0.30000000000000004, not exactly 0.3 +print(result) + +0.30000000000000004 + ``` -For financial calculations where precision is required, it's recommended to use the `decimal` module. +This isn't a Python bug, it's how fractional numbers are stored in binary: some decimal fractions can't be represented exactly. For financial calculations and other precision-critical tasks, use the `decimal` module. -### Complex numbers (complex) - -Complex numbers (type `complex`) contain real and imaginary parts, where the imaginary part is denoted by the suffix `j` or `J`: +For the same reason, **don't compare floats with `==` directly** β€” two seemingly equal fractions can differ in the last digit: ```python -# Creating complex numbers -z1 = 3 + 4j -z2 = complex(2, -3) # 2 - 3j - -# Getting real and imaginary parts -real_part = z1.real # 3.0 -imag_part = z1.imag # 4.0 +print(0.1 + 0.2 == 0.3) + +False + ``` -Complex numbers are often used in engineering calculations, signal processing, and mathematical algorithms. +If you need a comparison with tolerance, use `math.isclose(a, b)`: -## Arithmetic Operations +```python +import math +print(math.isclose(0.1 + 0.2, 0.3)) + +True + +``` -Python supports all standard arithmetic operations for numeric types: +## Arithmetic operations -### Basic operations +Python supports all the standard arithmetic operations: ```python a = 10 b = 3 -# Addition -sum_result = a + b # 13 - -# Subtraction -diff_result = a - b # 7 - -# Multiplication -mult_result = a * b # 30 +# Addition, subtraction, multiplication +a + b # 13 +a - b # 7 +a * b # 30 -# Division (always returns float) -div_result = a / b # 3.3333333333333335 +# Division always returns a float +a / b # 3.3333333333333335 -# Integer division (returns the integer part of the division result) -floor_div = a // b # 3 +# Integer division returns an int (drops the fractional part) +a // b # 3 -# Remainder of division -remainder = a % b # 1 +# Remainder (modulo) +a % b # 1 # Exponentiation -power = a ** b # 1000 +a ** b # 1000 ``` -### Mixed operations +### Mixed types -When performing operations between different numeric types, Python automatically converts types according to the following rule: -`int` β†’ `float` β†’ `complex` +If an expression contains both `int` and `float`, the result is always `float`: ```python -# int + float = float -mixed1 = 5 + 3.14 # 8.14 (float) - -# float + complex = complex -mixed2 = 2.5 + 3j # (2.5+3j) (complex) +5 + 3.14 # 8.14 (float) +10 * 2.0 # 20.0 (float) +4 ** 0.5 # 2.0 (float) ``` -## Rounding and Precision Control +## Rounding -Python provides several functions for rounding numbers: +Python offers several ways to round numbers: ```python -# Rounding to the nearest integer -rounded = round(3.14159) # 3 +# Round to the nearest integer +round(3.14159) # 3 -# Rounding to a specified number of decimal places -rounded_pi = round(3.14159, 2) # 3.14 +# Round to a specific number of decimal places +round(3.14159, 2) # 3.14 -# Rounding down (to the smaller integer) import math -floor_value = math.floor(3.99) # 3 -# Rounding up (to the larger integer) -ceil_value = math.ceil(3.01) # 4 +# Round down (toward smaller integer) +math.floor(3.99) # 3 + +# Round up (toward larger integer) +math.ceil(3.01) # 4 -# Truncating the fractional part -trunc_value = math.trunc(3.99) # 3 +# Truncate the fractional part +math.trunc(3.99) # 3 ``` -## Mathematical Functions +### Gotcha: round and banker's rounding -The `math` module provides many useful mathematical functions: +When a value lands exactly in the middle (e.g., `0.5` or `2.5`), Python rounds not to the larger integer but to the nearest **even** one. This is called _banker's rounding_ and is designed to reduce statistical bias when rounding large amounts of data: ```python -import math - -# Constants -pi = math.pi # 3.141592653589793 -e = math.e # 2.718281828459045 - -# Trigonometric functions (arguments in radians) -sin_value = math.sin(math.pi / 2) # 1.0 -cos_value = math.cos(math.pi) # -1.0 -tan_value = math.tan(math.pi / 4) # 1.0 - -# Inverse trigonometric functions -asin_value = math.asin(1.0) # 1.5707963267948966 (Ο€/2) - -# Logarithms -log_value = math.log(10) # natural logarithm, ln(10) = 2.302585092994046 -log10_value = math.log10(100) # logarithm base 10, log10(100) = 2.0 - -# Roots -sqrt_value = math.sqrt(16) # square root, 4.0 +print(round(0.5)) # we'd expect 1 + +0 + +print(round(1.5)) + +2 + +print(round(2.5)) # we'd expect 3 + +2 + +print(round(3.5)) + +4 + ``` -For working with complex numbers, the `cmath` module is used, which contains similar functions but works with complex arguments. +If you need "schoolbook" rounding (0.5 always goes up), write your own function or use `decimal` with an explicit rounding mode. -## Random Numbers +## Math functions -The `random` module is used to generate random numbers in Python: +The `math` module gives you useful functions: `sqrt` (square root), constants `pi` and `e`: ```python -import random - -# Random number from 0 to 1 -random_float = random.random() # e.g., 0.7593696548501615 - -# Random integer in a range -random_int = random.randint(1, 10) # random integer from 1 to 10 inclusive - -# Random floating-point number in a range -random_range = random.uniform(1.5, 3.5) # e.g., 2.4378123553729193 +import math -# Random choice from a sequence -random_choice = random.choice([1, 3, 5, 7, 9]) # random item from the list +math.sqrt(16) # 4.0 +math.pi # 3.141592653589793 +math.e # 2.718281828459045 ``` -## Numeric Methods and Functions +You'll also find trigonometry, logarithms, factorial and much more there. The full list is in the [math module documentation](https://docs.python.org/3/library/math.html). + +## Built-in functions for numbers -Python provides several built-in functions for working with numbers: +Some numeric functions are built straight into Python, without an `import`: ```python # Absolute value -abs_value = abs(-10) # 10 +abs(-10) # 10 -# Maximum and minimum value -max_value = max(1, 5, 3, 9, 2) # 9 -min_value = min(1, 5, 3, 9, 2) # 1 +# Max and min +max(1, 5, 3, 9, 2) # 9 +min(1, 5, 3, 9, 2) # 1 # Sum of a sequence -sum_value = sum([1, 2, 3, 4, 5]) # 15 +sum([1, 2, 3, 4, 5]) # 15 # Converting other types to numeric -int_from_str = int("42") # 42 -float_from_str = float("3.14") # 3.14 +int("42") # 42 +float("3.14") # 3.14 ``` - -## Understanding Check - -Let's check your understanding of numeric data types: - -**What will be the result of the expression `10 / 3` in Python?** diff --git a/course/module-1/sets/article.md b/course/module-1/sets/article.md index de428fa..ad63301 100644 --- a/course/module-1/sets/article.md +++ b/course/module-1/sets/article.md @@ -1,21 +1,27 @@ +--- +meta: + title: "Sets in Python" + description: "Detailed description of sets in Python: creation, methods for working with sets, set operations, and practical usage examples." +--- + # Sets in Python -In this article, we'll look at sets in Python β€” collections of unique elements with special properties that make them indispensable for certain tasks. +A set (`set`) in Python solves two specific tasks: fast membership testing (is an element in the collection?) and storing **only unique** values without duplicates. It's an unordered collection based on the mathematical concept of a set. ## What is a set? -A set in Python is an unordered collection of unique elements. Two key properties of sets are: +A set in Python is an unordered collection of unique elements. Two key properties of sets: -1. **Unordered**: elements have no specific order and are not indexed +1. **Unordered**: elements have no specific order and are not indexed β€” you can't reach for "the third element of a set", only iterate over all of them or check whether a particular one is present 2. **Unique**: each element appears only once Main characteristics of sets: -- **Mutability**: you can add and remove elements -- **Hashing**: elements must be hashable (immutable) objects -- **Efficiency**: optimized for fast membership testing +- **Mutability**: you can add and remove elements +- **Immutable elements**: only immutable objects can go inside a set (numbers, strings, tuples) +- **Efficiency**: optimized for fast membership testing -Sets are based on the mathematical concept of sets, making them ideal for union, intersection, and difference operations. +Because sets are based on the mathematical concept, they support union, intersection, and difference operations. ## Creating sets @@ -25,14 +31,23 @@ Sets are based on the mathematical concept of sets, making them ideal for union, # Set of integers numbers = {1, 2, 3, 4, 5} print(numbers) + +{1, 2, 3, 4, 5} + # Automatic duplicate removal duplicates = {1, 2, 2, 3, 3, 3, 4, 5, 5} print(duplicates) + +{1, 2, 3, 4, 5} + -# Set with different data types +# A single set can hold different immutable types mixed = {1, "hello", (1, 2, 3)} -print(mixed) +print(len(mixed)) # number, string and tuple β€” all three fit + +3 + ``` > **Important**: You cannot create an empty set using `{}`, as this will create an empty dictionary. To create an empty set, use `set()`. @@ -43,22 +58,23 @@ print(mixed) # Empty set empty_set = set() print(empty_set) + +set() + # Creating a set from a list numbers_set = set([1, 2, 2, 3, 4, 4, 5]) print(numbers_set) + +{1, 2, 3, 4, 5} + -# Creating a set from a string +# Creating a set from a string β€” repeated letters collapse letters = set("hello") -print(letters) # 'l' appears only once -``` - -### Using set comprehensions - -```python -# Set of squares of numbers from 0 to 9 -squares = {x**2 for x in range(10)} -print(squares) +print(len(letters)) # "hello" has two 'l', the set keeps one β€” 4 letters total + +4 + ``` ## Basic operations with sets @@ -69,98 +85,138 @@ print(squares) fruits = {"apple", "banana", "cherry"} print("apple" in fruits) + +True + print("pear" in fruits) + +False + ``` ### Adding and removing elements +The order of elements in a set is arbitrary, so in the string examples below we print them via `sorted()`, which returns a sorted list β€” that keeps the output from jumping around between runs. + ```python fruits = {"apple", "banana"} # Adding a single element fruits.add("cherry") -print(fruits) +print(sorted(fruits)) + +['apple', 'banana', 'cherry'] + # Adding multiple elements fruits.update(["pear", "orange"]) -print(fruits) +print(sorted(fruits)) + +['apple', 'banana', 'cherry', 'orange', 'pear'] + # Removing an element fruits.remove("banana") # raises KeyError if element doesn't exist -print(fruits) +print(sorted(fruits)) + +['apple', 'cherry', 'orange', 'pear'] + # Safely removing an element fruits.discard("cherry") # doesn't raise an error if element doesn't exist -print(fruits) - -# Removing and returning an arbitrary element -random_fruit = fruits.pop() -print(random_fruit) -print(fruits) +print(sorted(fruits)) + +['apple', 'orange', 'pear'] + + +# pop() removes and returns some element β€” which one exactly is not known in advance +removed = fruits.pop() +print(len(fruits)) # one fewer than before + +2 + # Clearing the set fruits.clear() print(fruits) + +set() + ``` -### Iterating through a set +### Looping over a set + +You go through a set with a `for` loop. The order is arbitrary and may change from run to run β€” that's what "unordered" means. When you need a predictable order, sort with `sorted()`: ```python colors = {"red", "blue", "green"} -for color in colors: +for color in sorted(colors): print(color) - -# The order of elements is not guaranteed! + +blue +green +red + ``` ## Mathematical set operations +Three main operations: union, intersection, and difference. They're easy to visualise with Venn diagrams: + +![Venn diagrams for the three set operations: union A | B, intersection A & B, and difference A - B](https://python-academy.org/static/guidePage/sets/set-operations-en.webp "Set operations") + ### Union +All elements from both sets: + ```python a = {1, 2, 3} b = {3, 4, 5} -# With the | operator union_set = a | b print(union_set) - -# With the union() method -union_set = a.union(b) -print(union_set) + +{1, 2, 3, 4, 5} + ``` +The same can be written as `a.union(b)`. + ### Intersection +Elements that are in both sets: + ```python a = {1, 2, 3, 4} b = {3, 4, 5, 6} -# With the & operator intersection_set = a & b print(intersection_set) - -# With the intersection() method -intersection_set = a.intersection(b) -print(intersection_set) + +{3, 4} + ``` +The same can be written as `a.intersection(b)`. + ### Difference +Elements from the first set that are not in the second: + ```python a = {1, 2, 3, 4} b = {3, 4, 5, 6} -# With the - operator difference_set = a - b print(difference_set) - -# With the difference() method -difference_set = a.difference(b) -print(difference_set) + +{1, 2} + ``` +The same can be written as `a.difference(b)`. + ## Comparing sets ```python @@ -170,18 +226,36 @@ c = {1, 2, 3} # Set equality print(a == c) # Contains the same elements + +True + # Subsets print(a.issubset(b)) # All elements of a are in b + +True + print(a < b) # a is a proper subset of b + +True + # Supersets print(b.issuperset(a)) # b contains all elements of a + +True + print(b > a) # b is a proper superset of a + +True + # Checking for no common elements d = {6, 7, 8} print(a.isdisjoint(d)) # No common elements + +True + ``` ## Immutable sets (frozenset) @@ -192,16 +266,25 @@ If you need an immutable version of a set, use `frozenset`: # Creating a frozenset immutable_set = frozenset([1, 2, 3, 4]) print(immutable_set) + +frozenset({1, 2, 3, 4}) + # Attempting to modify a frozenset raises an error try: immutable_set.add(5) except AttributeError as e: print(f"Error: {e}") + +Error: 'frozenset' object has no attribute 'add' + # frozenset can be used as a dictionary key or an element of another set normal_set = {frozenset([1, 2]), frozenset([3, 4])} -print(normal_set) +print(len(normal_set)) # both frozensets fit inside + +2 + ``` ## Practical examples of using sets @@ -212,6 +295,9 @@ print(normal_set) numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5] unique_numbers = list(set(numbers)) print(unique_numbers) + +[1, 2, 3, 4, 5] + ``` ### 2. Finding common elements @@ -222,15 +308,24 @@ users_group2 = ["Ivan", "Olga", "Elena", "Alex"] # Common elements (intersection) common_users = set(users_group1) & set(users_group2) -print(f"Users in both groups: {common_users}") +print(f"Users in both groups: {sorted(common_users)}") + +Users in both groups: ['Elena', 'Ivan'] + # Elements only in the first group (difference) only_group1 = set(users_group1) - set(users_group2) -print(f"Only in group 1: {only_group1}") +print(f"Only in group 1: {sorted(only_group1)}") + +Only in group 1: ['Anna', 'Maria', 'Peter'] + # All unique elements (union) all_users = set(users_group1) | set(users_group2) -print(f"All unique users: {all_users}") +print(f"All unique users: {sorted(all_users)}") + +All unique users: ['Alex', 'Anna', 'Elena', 'Ivan', 'Maria', 'Olga', 'Peter'] + ``` ### 3. Checking for uniqueness of elements @@ -241,7 +336,13 @@ def are_all_unique(items): return len(set(items)) == len(items) print(are_all_unique([1, 2, 3, 4, 5])) + +True + print(are_all_unique([1, 2, 3, 3, 4])) + +False + ``` ## Limitations and performance @@ -253,55 +354,78 @@ Set elements must be hashable (immutable): ```python # Works with immutable data types valid_set = {1, "hello", (1, 2, 3)} -print(valid_set) +print(len(valid_set)) # number, string and tuple are all hashable β€” all three fit + +3 + # Error with mutable data types try: invalid_set = {1, [2, 3], {"a": 1}} except TypeError as e: print(f"Error: {e}") + +Error: unhashable type: 'list' + ``` You can add: -- Numbers (int, float, complex) -- Strings (str) -- Tuples (tuple) with hashable elements -- Frozenset +- Numbers (int, float, complex) +- Strings (str) +- Tuples (tuple) with hashable elements +- Frozenset You cannot add: -- Lists (list) -- Dictionaries (dict) -- Sets (set) +- Lists (list) +- Dictionaries (dict) +- Sets (set) ### Performance -Sets are optimized for fast operations: +Fast lookup is exactly what sets are built for. Let's test on a million numbers: we look for the last one β€” the worst case for a list, which has to scan through everything. ```python import time -# Speed comparison (demonstration) -data = list(range(10000)) +data = list(range(1_000_000)) data_set = set(data) -# Search in list vs. search in set start = time.time() -9999 in data # Slow: O(n) +for _ in range(100): + 999_999 in data list_time = time.time() - start start = time.time() -9999 in data_set # Fast: O(1) +for _ in range(100): + 999_999 in data_set set_time = time.time() - start -print(f"Search in list: {list_time:.6f} sec") -print(f"Search in set: {set_time:.6f} sec") -print(f"Set is {list_time/set_time:.1f} times faster") +print(f"Search in list: {list_time:.3f} sec") + +Search in list: 0.442 sec + +print(f"Search in set: {set_time:.5f} sec") + +Search in set: 0.00001 sec + ``` +Your exact numbers will differ β€” they depend on the machine and how busy it is β€” but the gap stays just as wide: tens of thousands of times. The list has to check elements one by one until it finds the right one. Instead of scanning, a set computes straight away where the value should sit and checks only that spot β€” and it does so equally fast whether there are ten elements or a million. + Operations with O(1) complexity (constant time): -- Testing for membership: `x in set` -- Adding an element: `set.add(x)` -- Removing an element: `set.remove(x)`, `set.discard(x)` +- Testing for membership: `x in set` +- Adding an element: `set.add(x)` +- Removing an element: `set.remove(x)`, `set.discard(x)` + +## Check your understanding + +**What does `print(set([1, 2, 2, 3, 3, 3]))` output?** + +1. \{1, 2, 2, 3, 3, 3} β€” A set does not keep duplicates. Repeated values collapse, and each number stays in a single copy. + +2. **Correct answer:** \{1, 2, 3} β€” A set keeps only unique values, so the repeated 2s and 3s are dropped. This is exactly why set() is a common way to remove duplicates from a list. + +3. \[1, 2, 3] β€” The uniqueness is right, but the type is not: set() returns a set in curly braces, not a list. To get a list, wrap the result in list(). diff --git a/course/module-1/string-data-types/article.md b/course/module-1/string-data-types/article.md index 631295a..58f1ccd 100644 --- a/course/module-1/string-data-types/article.md +++ b/course/module-1/string-data-types/article.md @@ -1,128 +1,196 @@ -# String Data Types in Python +--- +meta: + title: "String data types in Python" + description: "Detailed description of string data in Python: creating strings, operations and methods for working with strings, string formatting." +--- -In this article, we'll get acquainted with strings in Python – one of the most important and frequently -used data types. +# String data types in Python -## What are strings in Python? +Almost everything a program shows a person and gets back from them is text: a name in a signup form, an error message, a line from a file. In Python, text lives in strings β€” values of type `str`. -Strings (type `str`) are sequences of characters that help us store and process textual information. - -They are needed literally everywhere: for displaying messages to users, storing names, addresses, working with files, and much more! +Strings are simply sequences of characters, and there's a lot you can do with them: join them together, slice them into pieces, search inside, insert variable values. Let's start with creating them. ## How to create strings in Python -Python offers several convenient ways to create strings. You can use single, double, or triple quotes: +Python offers several ways to create strings. You can use single, double, or triple quotes: ```python # Strings in single quotes single_quotes = 'Hello, world!' print(single_quotes) + +Hello, world! + # Strings in double quotes -double_quotes = "Python is awesome!" +double_quotes = "Python is fun" print(double_quotes) + +Python is fun + # Multi-line strings in triple quotes -multi_line = """This is a multi-line string. -It can span several lines -and preserves all line breaks.""" +multi_line = """Coffee recipe: +1. Boil water +2. Pour over the ground coffee +3. Wait 4 minutes""" print(multi_line) + +Coffee recipe: +1. Boil water +2. Pour over the ground coffee +3. Wait 4 minutes + ``` The choice of quote type depends on the situation: -- Single (`'`) and double (`"`) quotes work the same way -- If the string already contains single quotes, it's more convenient to wrap it in double quotes, and vice versa -- Triple quotes (`"""` or `'''`) are perfect for creating multi-line text +- Single (`'`) and double (`"`) quotes work the same way +- If the string already contains single quotes, it's easier to wrap it in double quotes, and vice versa +- Triple quotes (`"""` or `'''`) are good for multi-line text ### Escaping characters -Sometimes we need to include special characters in a string, such as quotes or newlines. For this, escape sequences with a backslash (`\`) are used: +Sometimes we need to include special characters in a string, such as quotes or a line break. Backslash sequences (`\`) handle this: ```python # Using quotes inside strings with escaping quote_inside = "He said: \"Hello!\"" print(quote_inside) + +He said: "Hello!" + path = "C:\\Program Files\\Python" print(path) + +C:\Program Files\Python + -# Commonly used escape sequences -newline = "First line.\nSecond line." # \n - new line +# Common escape sequences +newline = "First line.\nSecond line." # \n - newline print(newline) + +First line. +Second line. + tab = "Name:\tJohn" # \t - tab print(tab) + +Name: John + ``` -Here are useful escape sequences: +Useful escape sequences: -- `\n` β€” creates a new line -- `\t` β€” adds a tab (indentation) -- `\\` β€” allows inserting a backslash -- `\'` β€” adds a single quote -- `\"` β€” adds a double quote +- `\n` for a newline +- `\t` for a tab +- `\\` for a backslash +- `\'` for a single quote +- `\"` for a double quote -### Raw strings for paths and regular expressions +### Raw strings -If you're working with file paths or regular expressions, constantly escaping backslashes can be tedious. Python offers an elegant solution – "raw" strings with the prefix `r`: +When there are many backslashes β€” a Windows file path, say β€” escaping each one gets tedious. An `r` prefix before the string turns escaping off: every character is taken literally. ```python -# A normal string requires double backslashes -normal_path = "C:\\Users\\Username\\Documents" -print(normal_path) - -# A raw string takes backslashes literally -raw_path = r"C:\Users\Username\Documents" +# A regular string would need double backslashes +raw_path = r"C:\Users\Documents\file.txt" print(raw_path) + +C:\Users\Documents\file.txt + +``` + +## Immutability of strings + +Strings in Python are **immutable**. This means that once a string is created, you can't modify it "in place": every operation that looks like a modification actually creates a new string. +An attempt to change a character by index fails with an error: + +```python +language = "Python" + +try: + language[0] = "J" +except TypeError as e: + print(f"Error: {e}") + +Error: 'str' object does not support item assignment + ``` -Raw strings significantly simplify working with Windows paths and regular expressions! +To get a "modified" string, you build a new one and assign it, either to the same variable or to another: -## Basic operations with strings +```python +language = "Python" +language = "J" + "ython" +print(language) + +Jython + +``` -Now that we know how to create strings, let's learn how to work with them. Python offers many convenient operations for manipulating strings. +This rule applies to all string operations: `+`, `.upper()`, `.replace()` and others all return a new string instead of changing the original. -### String concatenation +## Basic string operations -Strings can be combined using the `+` operator to create new strings: +### Concatenation + +You can concatenate strings with the `+` operator to build new ones: ```python first_name = "John" -last_name = "Smith" +last_name = "Doe" full_name = first_name + " " + last_name print(full_name) + +John Doe + greeting = "Hello, " + full_name + "!" print(greeting) + +Hello, John Doe! + ``` -### String repetition +### Repetition -Using the `*` operator, you can repeat a string several times: +The `*` operator repeats a string multiple times: ```python -star_line = "*" * 10 -print(star_line) - border = "=" * 20 print(border) + +==================== + +``` -pattern = "+-" * 5 -print(pattern) +### String length -# You can create nice headers -title = "MENU" -decorated_title = f"{border}\n{title.center(20)}\n{border}" -print(decorated_title) -``` +To get the number of characters in a string, use the built-in `len()` function: -This is convenient for creating separators or visual text formatting! +```python +word = "Python" +print(len(word)) + +6 + + +empty = "" +print(len(empty)) + +0 + +``` ### Accessing individual characters -Strings in Python are sequences of characters, and we can access each character by its position (index). Indexing starts at 0: +Each character in a string can be accessed by its position (index). Indexing starts from 0, and negative indices count from the end: + +![String "Python" indices: positive 0–5 above letters, negative -6…-1 below them. The slice word[1:4] highlights y, t, h](https://python-academy.org/static/guidePage/string-data-types/indexing-en.webp "String indices and slices") ```python word = "Python" @@ -133,17 +201,26 @@ last_letter = word[5] print(f"First letter: {first_letter}") print(f"Second letter: {second_letter}") print(f"Last letter: {last_letter}") + +First letter: P +Second letter: y +Last letter: n + # You can also use negative indices to count from the end last_letter = word[-1] second_last_letter = word[-2] print(f"Last letter (from the end): {last_letter}") -print(f"Second-to-last letter: {second_last_letter}") +print(f"Second to last letter: {second_last_letter}") + +Last letter (from the end): n +Second to last letter: o + ``` -### Getting parts of a string (slices) +### Getting part of a string (slices) -One of the most powerful features of Python is slices. They allow you to extract substrings by specifying start and end indices: +Python has a powerful tool for working with strings: slices. They let you extract substrings by specifying the start and end indices: ```python message = "Python Programming" @@ -153,117 +230,261 @@ message = "Python Programming" first_word = message[0:6] # first 6 characters print(f"First word: {first_word}") + +First word: Python + -second_word = message[7:] # from the 7th to the end +second_word = message[7:] # from index 7 to the end print(f"Second word: {second_word}") + +Second word: Programming + -prefix = message[:6] # from the beginning to the 6th (not including the 6th) +prefix = message[:6] # from the start up to (not including) index 6 print(f"Prefix: {prefix}") + +Prefix: Python + -every_second = message[::2] # every second character -print(f"Every second letter: {every_second}") +every_second = message[::2] # every second character +print(f"Every other letter: {every_second}") + +Every other letter: Pto rgamn + reversed_string = message[::-1] # string in reverse -print(f"In reverse order: {reversed_string}") +print(f"In reverse: {reversed_string}") + +In reverse: gnimmargorP nohtyP + ``` -Slices are very flexible: - -- If you don't specify a start index, the slice will start from the beginning of the string -- If you don't specify an end index, the slice will continue to the end of the string -- A negative step allows you to traverse the string in reverse order +Slices are flexible: -## String immutability: an important feature! - -It's important to understand that strings in Python are immutable. -This means that after creating a string, you cannot change it – you need to create a new string: - -```python -language = "Python" -print(f"Original string: {language}") - -# Attempt to modify the string -try: - language[0] = "J" # Trying to change the first letter -except TypeError as e: - print(f"Error: {e}") - -# The correct way: create a new string -new_language = "J" + language[1:] -print(f"New string: {new_language}") -``` - -This feature may seem like a limitation, but it actually makes the code safer and more predictable! +- If you don't specify a start index, the slice begins at the start of the string +- If you don't specify an end index, the slice continues to the end of the string +- A negative step traverses the string in reverse ## Useful methods for working with strings -Python provides many built-in methods that make working with strings simple and convenient. Let's look at the most useful ones: +Python provides many built-in methods for working with strings. Let's look at the most useful ones. -### Changing text case +### Changing the case of text ```python -text = "Python Programming" +text = "Hello PYTHON world" upper_case = text.upper() -print(f"Upper case: {upper_case}") +print(f"Uppercase: {upper_case}") + +Uppercase: HELLO PYTHON WORLD + lower_case = text.lower() -print(f"Lower case: {lower_case}") +print(f"Lowercase: {lower_case}") + +Lowercase: hello python world + title_case = text.title() -print(f"Title case: {title_case}") - -capitalized = "hello world".capitalize() -print(f"Capitalized: {capitalized}") +print(f"Each word capitalized: {title_case}") + +Each word capitalized: Hello Python World + + +capitalized = text.capitalize() +print(f"Only first letter capitalized: {capitalized}") + +Only first letter capitalized: Hello python world + ``` -These methods are often used to standardize user input or format text. - -### Finding and replacing in strings +### Searching and replacing in strings ```python -text = "Python is an excellent programming language" - -# Finding a substring -position = text.find("excellent") -print(f"The word 'excellent' starts at position: {position}") - -count = text.count("o") -print(f"The letter 'o' appears {count} times") - -# Checking the beginning and end of a string +text = "Python is a great programming language" + +# Searching for a substring +position = text.find("great") +print(f"The word 'great' starts at position: {position}") + +The word 'great' starts at position: 12 + + +count = text.count("a") +print(f"The letter 'a' appears {count} times") + +The letter 'a' appears 5 times + + +# Checking the start and end of a string starts_with = text.startswith("Python") print(f"The string starts with 'Python': {starts_with}") + +The string starts with 'Python': True + ends_with = text.endswith("!") print(f"The string ends with '!': {ends_with}") + +The string ends with '!': False + # Checking for a substring -contains = "excellent" in text -print(f"The string contains the word 'excellent': {contains}") +contains = "great" in text +print(f"The string contains 'great': {contains}") + +The string contains 'great': True + # Replacing substrings -new_text = text.replace("excellent", "wonderful") +new_text = text.replace("great", "wonderful") print(f"Text after replacement: {new_text}") + +Text after replacement: Python is a wonderful programming language + ``` ### Splitting and joining strings -These methods are very useful when working with data: - ```python # Splitting a string into a list of words -sentence = "Python is an excellent programming language" +sentence = "Python is a great programming language" words = sentence.split() print(f"List of words: {words}") + +List of words: ['Python', 'is', 'a', 'great', 'programming', 'language'] + -# Splitting by a specific delimiter +# Splitting on a specific delimiter csv_data = "apple,banana,cherry" fruits = csv_data.split(",") print(f"List of fruits: {fruits}") + +List of fruits: ['apple', 'banana', 'cherry'] + # Joining a list into a string -words_to_join = ["Python", "is", "awesome"] +words_to_join = ["Python", "is", "fun"] joined_sentence = " ".join(words_to_join) print(f"Joined sentence: {joined_sentence}") + +Joined sentence: Python is fun + + +# Joining with another delimiter +path_parts = ["C:", "Users", "Username", "Documents"] +path = "\\".join(path_parts) +print(f"File path: {path}") + +File path: C:\Users\Username\Documents + +``` + +### Removing extra characters + +For cleaning up text from extra spaces or other characters: + +```python +text_with_spaces = " Python " + +# Removing spaces from both ends +cleaned = text_with_spaces.strip() +print(f"Without spaces: '{cleaned}'") + +Without spaces: 'Python' + + +left_cleaned = text_with_spaces.lstrip() +print(f"Without spaces on the left: '{left_cleaned}'") + +Without spaces on the left: 'Python ' + + +right_cleaned = text_with_spaces.rstrip() +print(f"Without spaces on the right: '{right_cleaned}'") + +Without spaces on the right: ' Python' + + +# Removing specific characters +text_with_dots = "...Python..." +without_dots = text_with_dots.strip('.') +print(f"Without dots: '{without_dots}'") + +Without dots: 'Python' + +``` + +### Method chaining + +Every string method returns a new string (remember the immutability rule). That means you can call them one after another via `.` β€” the result of one is fed straight into the next. This is called **method chaining**. + +For example, normalising user input: strip surrounding whitespace and lowercase it. + +In three steps with intermediate variables: + +```python +raw_input = " Hello " +without_spaces = raw_input.strip() +normalized = without_spaces.lower() +``` + +As a one-line chain: + +```python +raw_input = " Hello " +normalized = raw_input.strip().lower() +print(f"'{normalized}'") + +'hello' + ``` + +Chaining works because `strip()` returns a new string, on which `lower()` is then called immediately. It's a common Python pattern: instead of a bucket of intermediate variables, one expressive chain. + +## String formatting + +You often need to insert variable values into text. In modern Python the standard tool for this is the **f-string**: + +```python +name = "Anna" +age = 25 + +# Just add 'f' before the string and put variables inside {} +greeting = f"Hi, my name is {name} and I'm {age} years old." +print(greeting) + +Hi, my name is Anna and I'm 25 years old. + + +# Any expression can go inside {} +price = 19.99 +quantity = 3 +total = f"Total: ${price * quantity:.2f}" +print(total) + +Total: $59.97 + +``` + +Besides f-strings there are two other ways to format strings that you'll see in older code: the `"...".format(...)` method and the `"..." % (...)` operator. They aren't normally used in new code: f-strings are simpler and faster. + +## Understanding check + +Let's check how well you've understood working with strings: + +**What will be the result of the following code?** + +```python +text = "Python" +result = text[1:4] +``` + +1. "Pyt" β€” The slice \[1:4] starts at index 1 (the second character) and ends before index 4. + +2. **Correct answer:** "yth" β€” The slice \[1:4] on the string "Python" returns the characters at indices 1, 2, and 3, that is "yth". + +3. "ytho" β€” The slice \[1:4] extracts characters from index 1 through 3 inclusive, not including the character at index 4. + +4. "Pyth" β€” The slice \[1:4] starts at the second character (index 1), not the first. diff --git a/course/module-1/tuples/article.md b/course/module-1/tuples/article.md index 2022752..45ce268 100644 --- a/course/module-1/tuples/article.md +++ b/course/module-1/tuples/article.md @@ -1,28 +1,30 @@ -# Tuples in Python +--- +meta: + title: "Tuples in Python" + description: "Detailed description of tuples in Python: creation, methods for working with tuples, comparison with lists, and practical usage examples." +--- -In this article, we'll get acquainted with tuples β€” -ordered and immutable collections of data in Python. +# Tuples in Python -Tuples may seem very similar to lists, but there are important differences between them -that affect when and how they should be used. +When data has a **fixed shape** (coordinates, an RGB colour, a function return value made of several parts), that's a case for a tuple, not a list. ## What is a tuple? -A tuple in Python is an ordered, immutable collection of elements that can be of different types. Simply put, it's like a list, but one that cannot be changed after creation. +A tuple (`tuple`) in Python is an ordered, immutable collection of elements that can be of different types. Simply put, it's like a list, only you can't change it after creation. Main properties of tuples: -- **Ordered**: elements are stored in a specific order -- **Immutable**: after creation, you cannot add, remove, or change elements -- **Indexable**: elements can be accessed by their indices -- **Allow duplicates**: they can contain repeated values -- **Can contain different data types**: integers, strings, lists, etc. +- **Ordered**: elements are stored in a specific order +- **Immutable**: after creation, you cannot add, remove, or change elements +- **Indexable**: elements can be accessed by their indices +- **Allow duplicates**: they can contain repeated values +- **Can contain different data types**: integers, strings, lists, etc. ## Creating tuples There are several ways to create tuples in Python: -### 1. Using parentheses () +### Using parentheses () ```python # Empty tuple @@ -30,11 +32,17 @@ empty_tuple = () # Tuple with a single element (comma is required!) single_item = (42,) -type(single_item) +print(type(single_item)) + + + # Without a comma, it's just a number: (42) == 42 single_item_num = (42) -type(single_item_num) +print(type(single_item_num)) + + + # Tuple of numbers numbers = (1, 2, 3, 4, 5) @@ -46,21 +54,20 @@ mixed = (1, "hello", True, 3.14) nested = ((1, 2), ("a", "b"), (True, False)) ``` -### 2. Without parentheses (comma separation) +### Without parentheses (comma separation) Python allows creating tuples even without parentheses, simply by listing elements separated by commas: ```python # Creating a tuple without parentheses coordinates = 10.5, 20.7, 30.9 -type(coordinates) - -# Unpacking a tuple -x, y, z = coordinates -print(x, y, z) +print(type(coordinates)) + + + ``` -### 3. Using the tuple() constructor +### Using the tuple() constructor ```python # Creating an empty tuple @@ -69,14 +76,16 @@ empty_tuple = tuple() # Converting a list to a tuple list_to_tuple = tuple([1, 2, 3]) print(list_to_tuple) + +(1, 2, 3) + # Converting a string to a tuple (each character becomes an element) string_to_tuple = tuple("Python") print(string_to_tuple) - -# Converting a set to a tuple -set_to_tuple = tuple({1, 2, 3}) -print(set_to_tuple) + +('P', 'y', 't', 'h', 'o', 'n') + ``` ## Accessing tuple elements @@ -91,10 +100,16 @@ fruits = ("apple", "banana", "cherry", "date", "elderberry") # Getting elements by index first_fruit = fruits[0] print(first_fruit) + +apple + # Negative indices for accessing from the end of the tuple last_fruit = fruits[-1] print(last_fruit) + +elderberry + ``` ### Slices @@ -105,14 +120,23 @@ fruits = ("apple", "banana", "cherry", "date", "elderberry") # First three elements first_three = fruits[:3] print(first_three) + +('apple', 'banana', 'cherry') + # From second to fourth middle = fruits[1:4] print(middle) + +('banana', 'cherry', 'date') + # Reversing the tuple reversed_tuple = fruits[::-1] print(reversed_tuple) + +('elderberry', 'date', 'cherry', 'banana', 'apple') + ``` ## Tuple methods @@ -125,10 +149,16 @@ fruits = ("apple", "banana", "cherry", "banana", "date") # Counting the number of occurrences of an element banana_count = fruits.count("banana") print(banana_count) + +2 + # Finding the index of the first occurrence of an element banana_index = fruits.index("banana") print(banana_index) + +1 + ``` ## Operations with tuples @@ -137,32 +167,79 @@ print(banana_index) # Finding the length fruits = ("apple", "banana", "cherry") print(len(fruits)) + +3 + # Checking if an element exists print("apple" in fruits) + +True + print("mango" in fruits) + +False + # Concatenation (combining) tuples more_fruits = ("pear", "orange") all_fruits = fruits + more_fruits print(all_fruits) + +('apple', 'banana', 'cherry', 'pear', 'orange') + # Repetition repeated = fruits * 2 print(repeated) + +('apple', 'banana', 'cherry', 'apple', 'banana', 'cherry') + # Unpacking a, b, c = fruits print(a, b, c) + +apple banana cherry + +``` + +## Comparing tuples + +Tuples are compared **element by element**, left to right, until the first difference: + +```python +print((1, 2) < (1, 3)) + +True + +print((2, 0) < (1, 9)) + +False + ``` +In the first case the first elements are equal (`1 == 1`), so Python moves on to the second: `2 < 3`, so the whole tuple is smaller. In the second, `2 > 1` is enough on its own. + +This lets you sort a list of tuples by several fields with one call: + +```python +people = [("Bob", 30), ("Anna", 25), ("Anna", 30)] +print(sorted(people)) + +[('Anna', 25), ('Anna', 30), ('Bob', 30)] + +``` + +The sort first compares the first element (name); on a tie it falls back to the second (age). + ## Immutability of tuples It's important to understand that the immutability of tuples means that after creating a tuple you cannot: -- Modify existing elements -- Add new elements -- Remove elements +- Modify existing elements +- Add new elements +- Remove elements ```python # Creating a tuple @@ -177,6 +254,9 @@ coordinates = (10.5, 20.7, 30.9) # But you can create a new tuple based on an existing one new_coordinates = (15.0,) + coordinates[1:] print(new_coordinates) + +(15.0, 20.7, 30.9) + ``` ### Important note about nested mutable objects @@ -188,6 +268,9 @@ tuple_with_list = (1, 2, [3, 4]) # This works because we're modifying the list inside the tuple tuple_with_list[2][0] = 30 print(tuple_with_list) + +(1, 2, [30, 4]) + # But this will raise an error - cannot modify the tuple itself # tuple_with_list[2] = [5, 6] # TypeError @@ -207,6 +290,9 @@ def get_user_info(): # Unpacking the result user_name, user_age, user_is_admin = get_user_info() print(f"Name: {user_name}, Age: {user_age}, Admin: {user_is_admin}") + +Name: Anna, Age: 30, Admin: True + ``` ### 2. Fixed data @@ -218,10 +304,20 @@ DAYS_OF_WEEK = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturda # Usage today_index = 4 # Friday print(f"Today is {DAYS_OF_WEEK[today_index]}") + +Today is Friday + ``` ## Comparison of lists and tuples +Technically lists and tuples often overlap in what they can do, but in Python code they have **different roles**: + +- **A tuple is a "record"**: each position has a specific meaning. `(name, age, email)` isn't "a list of three random things", it's a structure where the first slot is always the name, the second always the age, the third always the email. +- **A list is a homogeneous collection**: all elements have roughly the same "kind", and their count can grow or shrink as the program runs. + +This distinction matters more than the technical details: even when both work, your choice affects readability and intent. + | Characteristic | List | Tuple | | ----------------------------- | ----------------------------- | ----------------- | | Syntax | `[1, 2, 3]` | `(1, 2, 3)` | @@ -240,10 +336,16 @@ print(f"Today is {DAYS_OF_WEEK[today_index]}") # Incorrect (this is not a tuple, just a number in parentheses) not_a_tuple = (42) print(type(not_a_tuple)) + + + # Correct single_item_tuple = (42,) print(type(single_item_tuple)) + + + ``` 2. **Attempting to modify a tuple** @@ -256,8 +358,21 @@ print(f"Today is {DAYS_OF_WEEK[today_index]}") coordinates[0] = 15.0 except TypeError as e: print(f"Error: {e}") + + Error: 'tuple' object does not support item assignment + ``` ## Understanding check **Which of the following statements about tuples in Python is true?** + +1. **Correct answer:** Tuples can be used as dictionary keys β€” Tuples, unlike lists, are immutable, so they can be used as dictionary keys. + +2. Tuples can be modified after creation β€” Tuples are immutable in Python. After creating a tuple, you cannot add, remove, or change its elements. + +3. A tuple with a single element is written as (42) β€” A tuple with a single element must contain a comma after the element: (42,). Without the comma, (42) will be treated as just a number in parentheses. + +4. Tuples can only contain elements of the same type β€” Tuples, like lists, can contain elements of different data types: numbers, strings, lists, other tuples, etc. + +In the next lesson we'll look at [sets](https://python-academy.org/en/guide/sets) (`set`) β€” collections where every element is unique. diff --git a/course/module-1/variable-scope/article.md b/course/module-1/variable-scope/article.md index bbb8e63..14b582d 100644 --- a/course/module-1/variable-scope/article.md +++ b/course/module-1/variable-scope/article.md @@ -1,29 +1,39 @@ -# Variable Scope in Python +--- +meta: + title: "Variable scope in Python" + description: "Local and global variables in Python, the global keyword, the typical UnboundLocalError trap, and how to properly pass data between functions." +--- -Today we'll explore an important concept in programming β€” variable scope. +# Variable scope in Python -If variables are "boxes" for storing data, then scope determines where these boxes are available and can be used. It's like different rooms in a house β€” some things are available only in a specific room, while others are available throughout the entire house! 🏠 +Python programs are almost never written as one long wall of code. They're assembled from **functions** β€” separate pieces of code, each with its own name and its own job. You declare a function with the word `def`, and `return` says what result it hands back. We'll cover them properly in their own chapter; for now it's enough to be able to read this: -## What is scope? +```python +def area_circle(radius): + result = 3.14 * radius * radius + return result -> Variable scope is the part of a program where a variable is accessible for use. It defines where and how a variable can be used in the code. +def area_square(side): + result = side * side + return result +``` -Understanding variable scope is an **important skill** that helps you prevent errors and write more structured code. In Python, variables can have different scopes, which affects their accessibility and lifetime. +Two functions here, and both use a variable named `result` inside. They don't interfere with each other: each function works with its own `result`, and one doesn't affect the other. -## Local and Global Variables +## What is variable scope? -In Python, there are two main types of variables in terms of scope: +The rule Python uses to decide which variable a name refers to at any given point in the code is called **variable scope**. It has two levels: -1. **Global variables** - created in the main body of the program and accessible in all parts of the program -2. **Local variables** - created inside functions and accessible only inside those functions +- **global** scope: anything declared in the main body of the program, visible inside every function +- **local** scope: variables declared inside a function, alive only while that function is running -Imagine a school: global variables are like announcements on the main bulletin board that are visible to everyone, while local variables are like notes on boards in individual classrooms, visible only to students in that particular class. +A useful image: scopes are like rooms in an apartment. Things on your personal desk are accessible only to you (a local variable inside a function). A note on the fridge is visible to the whole family (a global variable). -![Description of variable scope differences](https://python-academy.org/static/guidePage/variable-scope/ru_scope.png 'Illustration of local and global variables') +![Global and local variables: main program and two functions](https://python-academy.org/static/guidePage/variable-scope/scope-en.webp "Local and global variables") -### Global Variables +## Global variables -Global variables are defined outside of functions and can be used both inside functions and outside of them: +Global variables are defined outside any function and accessible both from within functions and outside them: ```python # Create a global variable @@ -33,150 +43,171 @@ def show_message(): # Use the global variable inside the function print(message) -show_message() # Call the function +show_message() + +Hello, world! + print(f"Variable outside the function: {message}") + +Variable outside the function: Hello, world! + ``` -Global variables are convenient when data needs to be accessible in all parts of the program, but their excessive use can make debugging and code maintenance more difficult. +The more globals you use, the harder the code is to debug: to understand what a function does, you have to look beyond its arguments and also remember which outside variables it reads. -### Local Variables +## Local variables -Local variables are created inside functions and exist only during the execution of that function. After the function completes, they are removed from memory: +Local variables are created inside a function and exist only while that function is running. Once it returns, they disappear from memory: ```python def calculate_sum(): - # Local variables a = 10 b = 20 result = a + b print(f"Sum inside the function: {result}") calculate_sum() + +Sum inside the function: 30 + -# Attempt to access a local variable outside the function +# Trying to access a local variable outside the function try: - print(f"Trying to access result: {result}") + print(f"result: {result}") except NameError as e: print(f"Error: {e}") + +Error: name 'result' is not defined + ``` -As you can see, the variable `result` exists only inside the `calculate_sum()` function and is not accessible outside of it. It's like leaving your belongings in a hotel room β€” they're only accessible while you're in the room! 🏨 +The variable `result` exists only inside `calculate_sum()` and doesn't leak out. -### Priority of Local Variables +## When names collide -When a variable is created inside a function with the same name as a global variable, Python gives priority to the local version: +If a function creates a variable with the same name as a global one, the local one takes precedence. This is called **shadowing**: the local "shadows" the global without touching it: ```python x = "global" def test_scope(): - x = "local" # Local variable with the same name + x = "local" print(f"Inside function: x = {x}") test_scope() + +Inside function: x = local + print(f"Outside function: x = {x}") + +Outside function: x = global + ``` -In this example, two different variables named `x` are created (local and global), despite having the same name. The local variable doesn't affect the global one and vice versa β€” they're like namesakes living in different cities! πŸ‘₯ - -This is very important to understand in order to avoid confusion in your code! +These are two completely different variables that happen to share a name. Like two people with the same name living in different cities: same name, different individuals. -## Modifying Global Variables +## Modifying a global variable from a function -What if you need to modify a global variable inside a function? By default, Python interprets assigning a value to a variable inside a function as creating a new local variable: +By default, any assignment inside a function is treated as creating a new local variable: ```python x = "global" def modify_global(): - # Python will create a new local variable x - x = "new local" # This variable doesn't change the global one + x = "new local" # This is a local, the global is unchanged print(f"Inside function: x = {x}") modify_global() + +Inside function: x = new local + print(f"Outside function: x = {x}") + +Outside function: x = global + ``` -As we can see, the function didn't change the global variable. To directly modify a global variable inside a function, the `global` keyword is used. - -### The global Keyword - -The `global` keyword tells Python that the variable should be taken from the global scope, rather than created locally: +To actually have a function change a global variable, you must explicitly say `global x` at the start of the function: ```python x = "global" def modify_global(): - global x # Indicate that we're using the global variable - x = "global modified" + global x + x = "global changed" print(f"Inside function: x = {x}") modify_global() + +Inside function: x = global changed + print(f"Outside function: x = {x}") + +Outside function: x = global changed + ``` -Now the function has successfully modified the global variable! It's like having a remote control that allows you to change things in another room! πŸ“± +The `global` keyword signals to anyone reading the code: "this function isn't just doing its own job, it reaches outside." For that reason, use it only when no other path is available. -The `global` keyword also allows creating new global variables inside functions: +## The most common trap: UnboundLocalError -```python -def create_global_var(): - global new_var # Create a global variable - new_var = "This variable will be accessible outside the function" - print("Variable created inside function") +A function reads a global variable and immediately reassigns it: -create_global_var() +```python +x = 10 -# Now the variable is accessible outside the function -print(f"Variable outside function: {new_var}") +def show(): + print(x) # read + x = x + 1 # and reassign right after ``` -But remember β€” with great power comes great responsibility! Use `global` only when it's truly necessary. +You'd expect the function to print `10` and bump `x` up to `11`. Instead, running it crashes with `UnboundLocalError: local variable 'x' referenced before assignment`. -## Best Practices for Working with Scope +What's going on? Python looks at the function **as a whole** before executing it and decides which names are local. It sees `x = ...` inside, so `x` is declared in this function and is therefore local. On the top line, `print(x)` then tries to read the **local** `x`, which doesn't yet have a value, and crashes. -When working with variable scope in Python, it's recommended to follow these practices: +For this function to work with the global `x`, you'd need `global x` at the top. But usually there's a cleaner path: don't touch the global, take `x` as a parameter and return the new value: -1. **Minimize the use of global variables** β€” they make code harder to understand and can lead to unpredictable results. +```python +def increment(value): + return value + 1 -2. **Pass data through function parameters** β€” this makes the code more explicit and modular: +x = 10 +x = increment(x) +print(x) + +11 + +``` + +## When global is justified, and when it isn't + +The most predictable way to handle data is: don't modify globals; pass values through parameters and return new ones via `return`: ```python def add(a, b): - result = a + b - return result + return a + b x = 5 y = 10 -sum_result = add(x, y) # Pass variables as arguments +sum_result = add(x, y) print(f"{x} + {y} = {sum_result}") + +5 + 10 = 15 + ``` -3. **Use return values instead of modifying global variables**: +This kind of function is honest: its result is fully determined by what came in. Nothing happens behind the scenes. That makes both testing and reading easier: a glance at the signature tells you everything. -```python -def increment(value): - return value + 1 - -count = 0 -print(f"Initial value: {count}") +`global` is justified when a variable really should live at program level: a configuration value, a shared counter. But in most practice tasks it isn't needed. -# Update the value through a return value -count = increment(count) -print(f"After increment: {count}") - -# We can call the function multiple times -count = increment(increment(count)) -print(f"After double increment: {count}") -``` +## Understanding check -## Understanding Check +Let's see how well you've understood variable scope. - +The interactive demonstration is available [in the Python Academy lesson](https://python-academy.org/en/guide/variable-scope). -Now you understand how variable scope works in Python. -This is an important concept that will help you write more structured, understandable, and predictable code. +In the next lesson we'll look at basic data types in Python: what's behind `int`, `str`, `float`, `bool`, and which operations are defined on each. diff --git a/course/module-1/variables/article.md b/course/module-1/variables/article.md index 3d8c3fa..5cf98b3 100644 --- a/course/module-1/variables/article.md +++ b/course/module-1/variables/article.md @@ -1,23 +1,35 @@ +--- +meta: + title: "Variables in Python" + description: "What variables are in Python. Creating variables, multiple assignment, naming rules and best practices." +--- + # Variables in Python -Hello! Today we'll get acquainted with one of the most important concepts in programming β€” variables. Think of them as labeled "boxes" (named memory areas) where you can store different things: numbers, texts, lists, and much more. +Suppose we're writing a greeting program. We don't know the user's name in advance: it will only appear when the program runs. So in our code we need a way to refer to that future name without knowing it yet. That's exactly what variables are for. -## What is a variable? +```python +name = "John" +print("Hello, " + name + "!") +``` -> A variable in Python is a named reference to an object in memory, whose type is determined dynamically and can change. +In this code `name` is a **label** that tells Python where the string `"John"` lives in memory. Whenever `name` appears later, Python looks at what the label points to and substitutes the value. -Working with variables in Python is extremely simple: +![A variable as a label pointing to an object in memory](https://python-academy.org/static/guidePage/variables/label-model-en.webp 'name β†’ "John"') -- No need to declare variables before using them -- No need to specify the type β€” Python will determine it automatically -- You can change the variable type at any time +## What is a variable? -A variable is created at the first assignment, and then you can use it anywhere in your program. +> A variable in Python is a name bound to an object in memory. Python infers the object's type automatically, and that binding can be changed at any time. + +Three conveniences follow from this: + +- you don't have to declare a variable beforehand: it appears on the first assignment +- you don't have to specify the type: Python sees what's on the right and remembers +- you can rebind a name to another value at any moment ## Creating and using variables -To create a variable, use the `=` operator. -Example: +A variable is created with the `=` operator. The name goes on the left, the value the name will point to goes on the right: ```python # Creating variables of different types @@ -28,83 +40,85 @@ is_student = True # Boolean variable courses = ["Python", "SQL", "JavaScript"] # List variable ``` -After this, when executing `print()` or other operations, the variable name will be replaced with its value: +Once created, you can use the name anywhere its value is needed: ```python name = "John" -# Using the variable in a greeting string -print("Hello, " + name + "! πŸ‘‹") +print("Hello, " + name + "!") + +Hello, John! + age = 25 -# Using the variable in a calculation next_year_age = age + 1 -print(f"Next year you will be {next_year_age} years old! πŸŽ‚") +print(f"Next year you'll be {next_year_age} years old") + +Next year you'll be 26 years old + ``` -## Multiple assignment - -Python allows you to assign values to multiple variables in a single line: - -```python -# Assigning the same value to multiple variables -x = y = z = 0 -print(f"x = {x}, y = {y}, z = {z}") +Notice the letter `f` before the quotes in the second example. It means that inside the string you can write variable names in curly braces, and Python will substitute their values. These are called f-strings, and you'll meet them often from here on β€” we'll cover them properly in the chapter on strings. -# Assigning different values to multiple variables -a, b, c = 1, 2, 3 -print(f"a = {a}, b = {b}, c = {c}") -``` +## Multiple assignment -You can also "unpack" a collection into multiple variables: +The most common use is **unpacking**: a collection on the right, several names on the left. They receive the values in order: ```python coordinates = (10, 20, 30) x, y, z = coordinates -print(f"Coordinates: x={x}, y={y}, z={z}") +print(f"x={x}, y={y}, z={z}") + +x=10, y=20, z=30 + ``` -To swap values between two variables without using an intermediate variable, use simple assignment: +The same logic gives an elegant way to swap two variables without a temporary one: ```python -# Swapping variable values a = 5 b = 10 -print(f"Before swap: a = {a}, b = {b}") +a, b = b, a +print(f"a = {a}, b = {b}") + +a = 10, b = 5 + +``` + +The right side first builds a tuple `(10, 5)`, then it's unpacked into the left side. No third variable needed. -a, b = b, a # Value swap -print(f"After swap: a = {a}, b = {b}") +You can also assign a single value to several names via a chain of `=`: + +```python +x = y = z = 0 ``` +This rarely shows up in real code; three separate lines are usually clearer. And with mutable objects the chain creates a single shared reference: `a = b = []` makes `a` and `b` the same list, which is almost never what you wanted. + ## Dynamic typing -Python is a dynamically typed language. The variable type is determined during execution and can change when a new value is assigned: +Python is a dynamically typed language. The type of a variable is determined at runtime and can change when a new value is assigned. You can check the current type with the `type()` function β€” it tells you what a value currently is: ```python -# Demonstrating variable type changes x = 10 # x has type int (integer) print(f"x = {x}, type: {type(x)}") + +x = 10, type: + x = "ten" # now x has type str (string) print(f"x = {x}, type: {type(x)}") + +x = ten, type: + x = [1, 2, 3] # now x has type list (list) print(f"x = {x}, type: {type(x)}") + +x = [1, 2, 3], type: + ``` -To determine the current data type of a variable, use the built-in `type()` function: - -```python -name = "John" -age = 25 -height = 1.85 -is_student = True - -# Determining variable types -print(f"{name}: {type(name)}") -print(f"{age}: {type(age)}") -print(f"{height}: {type(height)}") -print(f"{is_student}: {type(is_student)}") -``` +The types themselves β€” what `str`, `int`, `float` and `bool` are and how they differ β€” come later, in the lesson on basic data types. For now it's enough that a value has a type, and that the type can change. ## Naming variables in Python @@ -112,11 +126,11 @@ Good variable names make code understandable and maintainable. ### Naming rules -1. Names can contain letters, numbers, and underscores (`a-z`, `A-Z`, `0-9`, `_`). -2. Names must start with a letter or underscore. -3. You cannot use Python reserved words (e.g., `if`, `for`, `class`). +1. Names can contain letters, digits and the underscore (`a-z`, `A-Z`, `0-9`, `_`). +2. A name must start with a letter or underscore. +3. You can't use Python's reserved words (e.g., `if`, `for`, `class`). -```python +```python-interactive # Valid names name = "John" age_in_years = 25 @@ -130,72 +144,69 @@ _private_variable = "Non-public information" ### Best practices for naming variables -In the Python community, there is an official style guide β€” [PEP 8](https://peps.python.org/pep-0008/), which contains recommendations for naming variables and other aspects of writing clean, understandable Python code. - -Following these recommendations is considered good practice and helps write code that is easy to maintain: +The Python community has an official style guide, PEP 8. It's not law, but almost all Python code you'll encounter follows it. Your own code will be easier to read if it looks the same. -1. **Use descriptive names** β€” let the variable name indicate its purpose +1. **Use descriptive names**: let the name say what it's for ```python -# Better this way βœ… +# Better βœ… user_age = 25 -print(f"User age: {user_age}") # Than this ❌ a = 25 -print(f"Variable a value: {a}") # What is this variable about? ``` -2. **Use snake_case style** for variables (lowercase words separated by underscores) +A month later you'll come back to your own code, and `a` will tell you nothing. `user_age` is a free hint to the reader. + +2. **Use snake_case** (lowercase words joined by underscores) ```python # Python style βœ… first_name = "John" -print(f"Name: {first_name}") # Not Python style ❌ firstName = "John" -print(f"Name: {firstName}") ``` -3. **Add the prefix `is_` or `has_`** for boolean variables (bool type) +Technically both work. But all the Python code around you is in snake_case, and switching between styles within a project tires the eye. + +3. **Prefix `is_` or `has_`** for boolean variables ```python -# It's immediately clear these are boolean variables βœ… is_adult = True has_permission = False - -print(f"Is adult: {is_adult}") -print(f"Has permission: {has_permission}") ``` -4. **Use UPPER_CASE** for constants (values that shouldn't change) +When you see `if is_adult:`, it's immediately clear that the right side is a boolean, not, say, a number of years or a user object. Without the prefix, `adult` doesn't give you that hint. + +4. **UPPER_CASE** for constants + +```python +MAX_ATTEMPTS = 3 +PI = 3.14159 +``` - ```python - # Constants in uppercase βœ… - MAX_ATTEMPTS = 3 - PI = 3.14159 - ``` +Python has no true constants: you can rebind `MAX_ATTEMPTS` and the language won't complain. But the convention "anything in uppercase is off-limits" is universal, and everyone respects it. -5. **Avoid overly short and unclear names** β€” saving on characters leads to time wasted when reading code +5. **Avoid overly short names** - ```python - # Poor variable names ❌ - n = "John" # Too short - x = 25 # Unclear - flag = True # Vague purpose - str1 = "String" # Uninformative +```python +# Bad ❌ +n = "John" +flag = True +str1 = "String" + +# Good βœ… +user_name = "John" +is_verified = True +welcome_message = "Welcome!" +``` - # Good variable names βœ… - user_name = "John" - age_in_years = 25 - is_verified = True - welcome_message = "Welcome!" - ``` +A few extra characters at writing time save minutes at reading time. And code gets read far more often than it's written. ### Case sensitivity -Python is a case-sensitive language, which means there's a distinction between uppercase and lowercase letters in variable names. The variables `name`, `Name`, and `NAME` are interpreted as three different variables: +Python is case-sensitive, which means uppercase and lowercase letters are different in variable names. The variables `name`, `Name`, and `NAME` are interpreted as three different variables: ```python name = "John" @@ -203,21 +214,45 @@ Name = "Peter" NAME = "Alex" print(name) + +John + print(Name) + +Peter + print(NAME) + +Alex + ``` -These are three different variables: changing the case in a variable name creates a completely new variable. +Three different variables: changing the case in a variable name creates a completely new one. + +## Practice tasks + +Right below this article there's a block called **Practice tasks**. It's where you can immediately try out what you've just read. + +### What's already in the editor + +The editor on the right holds a starting point: the variable names from the task description and an `=` sign. All that's left is to fill in the values. + +![Task structure: the description with values on the left, the code starting point on the right, and the "Check" button that runs the check](https://python-academy.org/static/guidePage/variables/exercise-scaffold-en.webp "Task structure") + +The left side is the **task description**: what to name and with which values. The colored badges in the description (for example, `John`, `25`, `True`) are the exact values the code expects. + +The **"Check"** button runs your code and compares the result against the tests. You can press it as many times as you like. -## Understanding check +### What exactly gets checked -Let's check how well you've understood the features of working with variables in Python: +Below the editor there are tabs: "Result" and next to it "Test #1", "Test #2" and so on. A task only counts when every test passes. -**Which of the following statements most accurately describes variables in Python?** +Each test tab shows exactly what will be checked: a line of code and the value it should produce. For example, "Test #2" reads `user_age` and expects to find `25` there. You can open the tests at any point, including before the first check. +![How to read the result: every test has its own tab, a checkmark means it passed, and a failed test shows the value you actually got](https://python-academy.org/static/guidePage/variables/exercise-result-en.webp "Result structure") -## Conclusion +After the check each tab gets a mark: a green checkmark on the tests that passed, a red cross on the ones that didn't. A failed test opens on its own, and below its code an "Actual result" box appears with the value that actually came out. Compare it with the expected one and the difference becomes obvious. -Great! Now you know what variables are in Python and how to work with them. This is a fundamental concept that will be the foundation for all your future journey in the programming world. +The "Result" tab holds the overall summary (how many tests passed) along with everything you printed with `print()`. That comes in handy when you want to peek at intermediate values while solving. -Remember that proper variable naming and understanding their dynamic nature in Python will make your code more readable and maintainable. Experiment, practice, and you'll definitely succeed! +In the next lesson we'll wrap code into **functions**: how to give a piece of code a name, pass values into it and get a result back. diff --git a/course/module-2/attributes/article.md b/course/module-2/attributes/article.md index 305d19a..32f366e 100644 --- a/course/module-2/attributes/article.md +++ b/course/module-2/attributes/article.md @@ -1,84 +1,77 @@ -# Attributes and Methods in Python - -In the previous lesson we saw attributes (`self.name`) and methods (`def greet(self)`). Now let's go deeper: there are also **class attributes** (shared by every instance), methods split into three kinds (regular, class, static), and a classic mutable-default pitfall hiding inside `__init__` that catches even experienced developers. +--- +meta: + title: "Attributes and Special Methods" + description: "How class attributes differ from instance attributes, how to avoid the mutable default value trap, and why you need __str__ and __eq__." +--- -## Types of attributes +# Attributes and Special Methods -1. **Instance attributes**: unique to each object. -2. **Class attributes**: shared by all instances. +In the previous lesson all of an object's data lived in attributes like `self.name` β€” each object had its own. Today we'll look at data shared by all objects of a class, defuse a classic `__init__` trap, and teach objects to print and compare like proper values. -### Instance attributes +## Instance attributes and class attributes -> Instance attributes: variables that store data unique to each object. Usually created in `__init__` via `self.name = ...`. +The attributes that `__init__` creates via `self.name = ...` are called **instance attributes**: each object gets its own value. But some data is the same for everyone. Say all our students go to the same school β€” storing its name in every object makes no sense: the value is one for all, and if it changes, you'd have to update every object. -```python-executable -class Person: - def __init__(self, name, age): - # Instance attributes - self.name = name - self.age = age +Such data is declared right in the class body, outside the methods. That's a **class attribute**: - def birthday(self): - self.age += 1 - return f"{self.name} is now {self.age} years old!" +```python +class Student: + school = "School No. 1" # class attribute β€” one for all -# Create two different objects -person1 = Person("Anna", 25) -person2 = Person("Ivan", 30) + def __init__(self, name): + self.name = name # instance attribute β€” each has its own -# Each object has its own attribute values -print(f"{person1.name}: {person1.age} years old") -# Output: Anna: 25 years old +student1 = Student("Alex") +student2 = Student("Kate") -# Changing one object's attribute doesn't affect another -print(person1.birthday()) -# Output: Anna is now 26 years old! -print(f"{person2.name}: {person2.age} years old") # Age hasn't changed -# Output: Ivan: 30 years old +print(f"{student1.name}, {student1.school}") + +Alex, School No. 1 + +print(f"{student2.name}, {student2.school}") + +Kate, School No. 1 + ``` -### Class attributes - -> Class attributes: variables declared right inside the class body (outside any methods). They are shared by all instances. +Notice: `student1.school` reads fine even though the object itself has no such attribute. When Python doesn't find the name in the instance, it looks it up in the class. So changing the class attribute is enough β€” all objects see the change at once: -```python-executable +```python class Student: - # Class attribute: shared by all instances - school = "School #1" + school = "School No. 1" def __init__(self, name): - # Instance attribute: one per student self.name = name -# Create students student1 = Student("Alex") student2 = Student("Kate") -# Each has their own name; school is shared -print(f"{student1.name}, {student1.school}") -# Output: Alex, School #1 +Student.school = "School No. 5" -# Change the class attribute: every instance sees it immediately -Student.school = "Gymnasium #5" print(f"{student1.name}, {student1.school}") -# Output: Alex, Gymnasium #5 + +Alex, School No. 5 + print(f"{student2.name}, {student2.school}") -# Output: Kate, Gymnasium #5 + +Kate, School No. 5 + ``` -### When to use which +`class Student`: `school = "School No. 1"` -- **Instance attributes**: for data that varies between objects (name, age, id). -- **Class attributes**: for: - - Constants and default values; - - Data shared by every instance (e.g. the school name across all students); - - Class-level counters (e.g. how many objects have been created). +- `student1`: `name = "Alex"`, `school` β€” from the class +- `student2`: `name = "Kate"`, `school` β€” from the class -## The mutable-default pitfall +`name` β€” each object has its own, while `school` lives in the class: one for all. -A classic Python trap that catches even seasoned developers. Suppose we want a student to start with an empty list of grades. It seems natural to write: +The rule of thumb is simple: whatever differs between objects goes into instance attributes (name, age), and whatever is one for all goes into class attributes (constants, shared settings, default values). -```python-executable +## The mutable default value trap + +This `__init__` trap catches even experienced developers. Say we want a student to have an empty list of grades by default. It seems logical to write this: + +```python class Student: def __init__(self, name, grades=[]): # looks harmless self.name = name @@ -87,186 +80,141 @@ class Student: s1 = Student("Anna") s1.grades.append(5) print(s1.grades) -# Output: [5] + +[5] + s2 = Student("Ivan") -print(s2.grades) # we expect [] -# Output: [5] +print(s2.grades) # expecting [] + +[5] + ``` -Ivan inherited Anna's grade even though we never added anything to his list. Why? +Ivan ended up with Anna's grade, even though we added nothing to him. Why? -Default parameter values are evaluated **once**, when the function is defined, not on every call. The list `[]` is created once and reused by every call to `Student(...)` that doesn't pass a `grades` argument. So `s1.grades` and `s2.grades` point to **the same** list in memory. +Default parameter values are evaluated **once**, at the moment the function is defined β€” not on every call. The list `[]` was created once and is reused by every `Student(...)` call without a `grades` argument. In other words, `s1.grades` and `s2.grades` point to **the very same** list in memory. -The fix: default to `None`, and create the real list inside: +The correct pattern: use `None` as the default β€” the technique you know from the previous lesson β€” and create the real list inside: -```python-executable +```python class Student: def __init__(self, name, grades=None): self.name = name - self.grades = grades if grades is not None else [] + if grades is None: + grades = [] + self.grades = grades s1 = Student("Anna") s1.grades.append(5) print(s1.grades) -# Output: [5] + +[5] + s2 = Student("Ivan") print(s2.grades) -# Output: [] + +[] + ``` -Now each object gets its own fresh list. The same trick works for dicts, sets, and any other mutable default. - -## Types of methods - -In Python there are several kinds of methods: - -1. **Regular methods** (instance methods): work with a specific object via `self`. -2. **Class methods**: work with the class as a whole, receive `cls`. -3. **Static methods**: receive neither `self` nor `cls`. -4. **Special methods**: have special meaning to Python (e.g. `__init__`, `__str__`). +Now every object gets its own empty list. The same trick works for dictionaries, sets and any other mutable values. -### Regular methods +## Special methods -> Regular methods: functions inside a class that take `self` as their first parameter. Through `self` they access the object's attributes and call other methods. +The data is neatly stored in attributes β€” now let's see how the object behaves in everyday operations. Print it and compare two identical ones: -```python-executable -class Rectangle: - def __init__(self, width, height): - self.width = width - self.height = height - - def area(self): - return self.width * self.height +```python +class Vector: + def __init__(self, x, y): + self.x = x + self.y = y - def perimeter(self): - return 2 * (self.width + self.height) +v1 = Vector(3, 4) +v3 = Vector(3, 4) -# Create a rectangle and call methods -rect = Rectangle(5, 3) -print(f"Area: {rect.area()}") -# Output: Area: 15 -print(f"Perimeter: {rect.perimeter()}") -# Output: Perimeter: 16 +print(v1) + +<__main__.Vector object at 0x7f9b1c2d3e50> + +print(v1 == v3) + +False + ``` -### Class methods - -> Class methods: take the class itself as the first parameter (usually called `cls`), not an instance. Decorated with `@classmethod`. Often used as alternative constructors. +Printing produced the object's memory address instead of its contents, and the comparison answered `False` despite equal coordinates: without our help, Python compares objects by whether they are the same object in memory, not by content. -```python-executable -class Date: - def __init__(self, day, month, year): - self.day = day - self.month = month - self.year = year +Both problems are solved with **special methods** β€” methods with double underscores in the name that Python calls itself at the right moment: - def display(self): - return f"{self.day:02d}.{self.month:02d}.{self.year}" +- `__init__` you already write in every class: it fires on `Student(...)`; +- `__str__` fires when the object needs to become a string β€” inside `print()`, for example; +- `__eq__` β€” on comparison with `==`; +- `__add__` β€” on addition with `+`. - # Class method: alternative constructor - @classmethod - def from_string(cls, date_string): - day, month, year = map(int, date_string.split('.')) - return cls(day, month, year) - -# Create the standard way -date1 = Date(15, 6, 2023) -print(date1.display()) -# Output: 15.06.2023 - -# Create using a class method -date2 = Date.from_string("25.12.2023") -print(date2.display()) -# Output: 25.12.2023 -``` +Let's define the three new ones in `Vector`: -### Static methods - -> Static methods: take neither `self` nor `cls`. Decorated with `@staticmethod`. Used for utility functions logically related to a class but not needing access to its state. - -```python-executable -class MathUtils: - @staticmethod - def is_prime(number): - """Checks if a number is prime""" - if number < 2: - return False - for i in range(2, int(number**0.5) + 1): - if number % i == 0: - return False - return True - -# Call a static method through the class name -print(f"Is 7 a prime number: {MathUtils.is_prime(7)}") -# Output: Is 7 a prime number: True -print(f"Is 10 a prime number: {MathUtils.is_prime(10)}") -# Output: Is 10 a prime number: False -``` - -### Special methods - -> Special methods: methods whose names start and end with double underscores (`__init__`, `__str__`, `__add__`, `__eq__`). Python calls them automatically for specific operations: creating an object, printing, addition, comparison. - -```python-executable +```python class Vector: def __init__(self, x, y): self.x = x self.y = y - # String representation: called by print() and str() def __str__(self): return f"Vector({self.x}, {self.y})" - # Overload + operator def __add__(self, other): return Vector(self.x + other.x, self.y + other.y) - # Overload == operator def __eq__(self, other): return self.x == other.x and self.y == other.y -# Create vectors and use the overloaded operators v1 = Vector(3, 4) v2 = Vector(1, 2) v3 = Vector(3, 4) -print(f"v1 = {v1}") # calls __str__ -# Output: v1 = Vector(3, 4) -print(f"v1 + v2 = {v1 + v2}") # calls __add__ -# Output: v1 + v2 = Vector(4, 6) -print(v1 == v2) # calls __eq__ -# Output: False -print(v1 == v3) # calls __eq__ -# Output: True +print(v1) + +Vector(3, 4) + +print(v1 + v2) + +Vector(4, 6) + +print(v1 == v3) + +True + ``` -Without `__eq__`, comparing `v1 == v3` would have returned `False` even though their coordinates match: by default Python compares objects by identity (are they the same object in memory?), not by content. Defining `__eq__` lets us say: "treat these objects as equal when their coordinates match." +Note `__add__`: it doesn't change the original vectors β€” it builds a new object out of their coordinates and returns it. + +Python has dozens of special methods: `__len__` teaches an object to answer `len(...)`, `__repr__` β€” to show itself while debugging. No need to memorize the list β€” they'll come up throughout the course as needed. -## Commonly used special methods +## Understanding check -Here are some of the most commonly used special methods in Python: +**The program created two students. What does the last line print?** -| Method | Description | Usage Example | -| ------------------------------- | ------------------------------------ | ------------------ | -| `__init__(self, ...)` | Constructor | `obj = MyClass()` | -| `__str__(self)` | String representation for users | `print(obj)` | -| `__repr__(self)` | String representation for developers | `repr(obj)` | -| `__len__(self)` | Object length | `len(obj)` | -| `__getitem__(self, key)` | Access by key/index | `obj[key]` | -| `__setitem__(self, key, value)` | Assignment by key/index | `obj[key] = value` | -| `__call__(self, ...)` | Calling an object as a function | `obj()` | -| `__add__(self, other)` | + operator | `obj + other` | -| `__sub__(self, other)` | - operator | `obj - other` | -| `__eq__(self, other)` | == operator | `obj == other` | -| `__lt__(self, other)` | < operator | `obj < other` | +```python +class Student: + def __init__(self, name, grades=[]): + self.name = name + self.grades = grades -## What's next? +s1 = Student("Anna") +s1.grades.append(5) -In the next lessons we'll go through the four principles of OOP one at a time. Starting with inheritance: how one class extends another, reusing its attributes and methods. +s2 = Student("Ivan") +print(s2.grades) +``` ---- +1. \[] β€” The default list is not created on every call β€” it is created once: s2 got the very list that 5 was already appended to. + +2. **Correct answer:** \[5] β€” The default value was evaluated once when \_\_init\_\_ was defined, so s1.grades and s2.grades are the same list. + +3. An error: s2 has no grades attribute. β€” The attribute exists: \_\_init\_\_ stored the default list into self.grades. -**Which decorator is used to create class methods in Python?** +4. \[5, 5] β€” The 5 was appended once, so the list holds a single value β€” even though the list is shared. +In the next lesson we'll look at inheritance: how one class continues another, reusing its attributes and methods. diff --git a/course/module-2/built-in-libraries/article.md b/course/module-2/built-in-libraries/article.md index 960a254..3fd30f1 100644 --- a/course/module-2/built-in-libraries/article.md +++ b/course/module-2/built-in-libraries/article.md @@ -1,26 +1,22 @@ -# Python Built-in Libraries +--- +meta: + title: "Python's built-in libraries" + description: "An overview of the most useful built-in Python libraries, what they do, and practical usage examples." +--- -Imagine you just got a new smartphone. Right after purchase, it already has a calendar, calculator, camera, and other useful applications installed. Python's built-in libraries work in a similar way! 🧰 +# Python's built-in libraries -Built-in libraries are modules that come with Python and are available immediately after installation. They provide ready-made solutions for the most common programming tasks. +Compute a factorial, find today's date, shuffle a list, read JSON β€” none of this needs anything installed. It already ships with Python out of the box: dozens of ready-made modules available right after an `import`, with no `pip install`. This is the standard (built-in) library. ## What are built-in libraries? -> Built-in libraries (standard library) are a set of modules that are included in the Python distribution and can be used without additional installation. +> Built-in libraries (the standard library) are a set of modules included in the Python distribution that you can use without any additional installation. -The Python standard library includes: +## The main built-in libraries -- Modules for system interaction -- Tools for data processing -- Utilities for network operations -- Tools for creating user interfaces -- And much more +Let's go through several of the most useful built-in libraries in Python. -## Main built-in libraries - -Let's look at several of the most useful Python built-in libraries. - -### math β€” mathematical functions +### math: mathematical functions The `math` module provides access to mathematical functions defined in the C language standard: @@ -29,145 +25,131 @@ import math # Constants print(f"Number Ο€: {math.pi}") + +Number Ο€: 3.141592653589793 + print(f"Number e: {math.e}") + +Number e: 2.718281828459045 + # Trigonometric functions angle = math.pi / 4 # 45 degrees in radians print(f"Sine of 45Β°: {math.sin(angle):.4f}") + +Sine of 45Β°: 0.7071 + print(f"Cosine of 45Β°: {math.cos(angle):.4f}") + +Cosine of 45Β°: 0.7071 + # Other functions print(f"Factorial of 5: {math.factorial(5)}") + +Factorial of 5: 120 + print(f"Greatest common divisor of 12 and 18: {math.gcd(12, 18)}") + +Greatest common divisor of 12 and 18: 6 + ``` -### random β€” generating random numbers +### random: generating random numbers -The `random` module provides functions for generating random numbers and selecting random elements: +The `random` module provides functions for generating random numbers and picking random elements: ```python import random # Generating a random integer in a range -print(f"Random number from 1 to 10: {random.randint(1, 10)}") - -# Random floating-point number from 0 to 1 -print(f"Random number from 0 to 1: {random.random():.4f}") - -# Selecting a random element from a sequence +print(f"Random number between 1 and 10: {random.randint(1, 10)}") + +Random number between 1 and 10: 7 + + +# Random float between 0 and 1 +print(f"Random number between 0 and 1: {random.random():.4f}") + +Random number between 0 and 1: 0.3528 + + +# Picking a random element from a sequence fruits = ["apple", "banana", "orange", "pear"] print(f"Random fruit: {random.choice(fruits)}") + +Random fruit: orange + # Shuffling a sequence numbers = [1, 2, 3, 4, 5] random.shuffle(numbers) print(f"Shuffled numbers: {numbers}") + +Shuffled numbers: [3, 1, 5, 2, 4] + ``` -### datetime β€” working with dates and time +### datetime: dates and times -The `datetime` module provides classes for working with dates and time: +The `datetime` module can parse dates from strings, add intervals to them, and format them back. That set is enough for typical date operations: ```python -import datetime +from datetime import datetime, timedelta -# Current date and time -now = datetime.datetime.now() -print(f"Current date and time: {now}") +# parse a string into a date using the "day.month.year" pattern +d = datetime.strptime("31.12.2022", "%d.%m.%Y") -# Creating a specific date -specific_date = datetime.date(2023, 12, 31) -print(f"Specified date: {specific_date}") +# add an interval +new_d = d + timedelta(days=5) -# Difference between dates -today = datetime.date.today() -new_year = datetime.date(today.year + 1, 1, 1) -days_until_new_year = (new_year - today).days -print(f"Days until New Year: {days_until_new_year}") - -# Formatting dates -formatted_date = now.strftime("%d.%m.%Y %H:%M") -print(f"Formatted date: {formatted_date}") +# format the date back into a string +print(new_d.strftime("%d.%m.%Y")) + +05.01.2023 + ``` -### os β€” interacting with the operating system - -The `os` module provides functions for interacting with the operating system: +This is just the tip of the iceberg β€” `datetime` can do a lot more, and it has its own chapter, "Working with dates and times", later in the module. -```python -import os +Two more big standard-library topics have chapters of their own, so here we'll just name the modules: -# Getting the current working directory -print(f"Current directory: {os.getcwd()}") +| What you need | Module | Where we cover it | +| --------------- | --------------- | ---------------------- | +| Files and paths | `os`, `pathlib` | "Working with files" | +| JSON and CSV | `json`, `csv` | "JSON and CSV formats" | -# List of files and folders in a directory -files = os.listdir('.') -print(f"First 3 files in the current directory: {files[:3]}") +But `collections` didn't get its own chapter, so let's look at it right now. -# System information -print(f"Operating system name: {os.name}") +### collections: specialised data types -# Check if a file or directory exists -file_exists = os.path.exists('example.txt') -print(f"File example.txt exists: {file_exists}") -``` +The `collections` module provides several convenient data structures on top of the built-in ones. One of the most useful is `Counter` for counting elements: -### json β€” working with JSON format +```python +from collections import Counter -The `json` module provides functions for working with data in JSON format: +orders = ["apple", "banana", "apple", "cherry", "apple", "banana"] +counts = Counter(orders) -```python -import json - -# Python dictionary -person = { - "name": "John", - "age": 30, - "city": "New York", - "languages": ["Python", "JavaScript", "SQL"] -} - -# Converting a dictionary to a JSON string -person_json = json.dumps(person, indent=4) -print("JSON string:") -print(person_json) - -# Converting a JSON string to a Python object -json_string = '{"name": "Mary", "age": 25, "city": "San Francisco"}' -person_dict = json.loads(json_string) -print(f"Name: {person_dict['name']}, Age: {person_dict['age']}") +# most_common() sorts by count, highest first +print(counts.most_common()) + +[('apple', 3), ('banana', 2), ('cherry', 1)] + ``` -### collections β€” specialized data types +The module also has `defaultdict`, `namedtuple`, `deque` and others, advanced tools that will come in handy later. -The `collections` module provides alternative data structures for Python: +## Understanding check -```python -from collections import Counter, defaultdict, namedtuple - -# Counter - counting elements -text = "Programming in Python is interesting and enjoyable" -character_count = Counter(text.lower()) -print("Three most frequent characters:") -for char, count in character_count.most_common(3): - print(f"'{char}': {count}") - -# defaultdict - dictionary with default value -fruit_categories = defaultdict(list) -fruit_categories["yellow"].append("banana") -fruit_categories["red"].append("apple") -fruit_categories["red"].append("strawberry") -print(f"Yellow fruits: {fruit_categories['yellow']}") -print(f"Green fruits: {fruit_categories['green']}") # Empty list - -# namedtuple - named tuples -Person = namedtuple('Person', ['name', 'age', 'job']) -alice = Person('Alice', 30, 'engineer') -print(f"{alice.name}, {alice.age} years old, works as an {alice.job}") -``` +**Which library is best suited for working with dates in Python?** -## Understanding Check +1. math β€” The math library is meant for mathematical computation, not for working with dates. -Let's check how well you've understood the topic of built-in libraries: +2. **Correct answer:** datetime β€” The datetime library is purpose-built for working with dates and times. -**Which library is best suited for working with dates in Python?** +3. random β€” The random library is used for generating random numbers, not for working with dates. + +4. json β€” The json library is meant for working with the JSON format, not for dates. + +In the next lesson we'll look at [third-party libraries](https://python-academy.org/en/guide/third-party-libraries), the ones installed via `pip install` that extend Python beyond the standard distribution. diff --git a/course/module-2/classes/article.md b/course/module-2/classes/article.md index 74b8c1f..897f9eb 100644 --- a/course/module-2/classes/article.md +++ b/course/module-2/classes/article.md @@ -1,36 +1,108 @@ +--- +meta: + title: "Classes and Objects" + description: "Building your first class step by step: __init__, self and methods. Then deeper: how self works, object mutability and dynamic attributes." +--- + # Classes and Objects -In the previous lesson we covered the basics: a class is a template, an object is a filled-in instance, and methods take `self` as their first parameter. In this article we'll go deeper on three topics: how `self` **actually** works under the hood, the fact that objects in Python are **mutable**, and why you can add attributes **on the fly** (and why you usually shouldn't). +In the previous chapter we looked at a class from a distance: the shape, the template, the objects. Now let's build such a class ourselves, line by line. + +## Building a class step by step + +The shortest possible class in Python looks like this: + +```python +class Person: + pass + +person = Person() +print(type(person)) + + + +``` + +`class Person:` declares the class, and `Person()` creates an object from it. An empty one, though: there is no data inside yet. + +The data is set up in `__init__`, a special method that Python calls itself on every `Person(...)`. The call's arguments land in its parameters: + +```python +class Person: + def __init__(self, name, age): + self.name = name + self.age = age + +person = Person("Anna", 25) +print(person.name) + +Anna + +print(person.age) + +25 + +``` + +The line `self.name = name` reads: "take the parameter `name` and store it in the object under the name `name`". On the left is the object's attribute, on the right the method's parameter. + +This is the template-filling from the diagram in the previous chapter: the call `Person("Anna", 25)` filled in the blanks. + +The data is there, so let's add the action. A method is declared like an ordinary function, only inside the class, and it always takes `self` as its first parameter: + +```python +class Person: + def __init__(self, name, age): + self.name = name + self.age = age + + def greet(self): + return f"Hi, my name is {self.name}, I am {self.age} years old." + +person = Person("Anna", 25) +print(person.greet()) + +Hi, my name is Anna, I am 25 years old. + +``` + +Spotted the oddity? `def greet(self)` declares a parameter, yet `person.greet()` passes nothing. So where does `self` come from? ## How self works -When you write `person.greet()`, Python internally turns this into `Person.greet(person)`. The object on the left of the dot automatically becomes the first argument of the method β€” that's the `self` you see in the method signature. +The `greet` method is written in the class once, while there can be any number of objects. So on a call the method somehow needs to know **whose** `name` to print. That's the dot's job: Python executes the call `person.greet()` as `Person.greet(person)` β€” the object to the left of the dot becomes the first argument itself. That is what arrives in the `self` parameter. + +`person.greet()` β€” Python executes as β†’ `Person.greet(person)`; the object to the left of the dot becomes the first argument and arrives in `self`. -Through `self`, the method sees its own data: +Let's check that this is literally true: -```python-executable +```python class Person: def __init__(self, name, age): self.name = name self.age = age def greet(self): - return f"Hi, my name is {self.name}, I'm {self.age} years old." + return f"Hi, my name is {self.name}, I am {self.age} years old." person = Person("Anna", 25) # These two calls do the same thing: print(person.greet()) -# Output: Hi, my name is Anna, I'm 25 years old. + +Hi, my name is Anna, I am 25 years old. + print(Person.greet(person)) -# Output: Hi, my name is Anna, I'm 25 years old. + +Hi, my name is Anna, I am 25 years old. + ``` -`self` isn't a keyword or magic. It's just the conventional name for a method's first parameter. Technically you can call it whatever you want (`def greet(this):` works too), but the Python community expects `self` and linters will complain about other names. +Since `self` is an ordinary first parameter, it could have any name: `def greet(this):` works too. But the whole Python community writes `self`, and linters complain about anything else. -Through `self`, methods can call other methods on the same object: +Through `self`, methods can call other methods of the same object: -```python-executable +```python class Person: def __init__(self, name, age): self.name = name @@ -40,19 +112,21 @@ class Person: return self.age >= 18 def describe(self): - status = "an adult" if self.is_adult() else "a minor" + status = "adult" if self.is_adult() else "minor" return f"{self.name}: {status}" person = Person("Anna", 25) print(person.describe()) -# Output: Anna: an adult + +Anna: adult + ``` ## Objects in Python are mutable -After an object is created, its state can change: by calling methods that modify attributes, or by assigning to attributes directly. +So far the methods have only read the data: `is_adult` looked at `self.age` and changed nothing. But an object's state can also be changed: with a method, like `add_grade` below, or by assigning to an attribute directly. -```python-executable +```python class Student: def __init__(self, name): self.name = name @@ -65,67 +139,86 @@ class Student: def average_grade(self): if not self.grades: return "No grades" - return sum(self.grades) / len(self.grades) + return round(sum(self.grades) / len(self.grades), 1) student = Student("Maria") -print(f"Average: {student.average_grade()}") -# Output: Average: No grades +print(f"Average grade: {student.average_grade()}") + +Average grade: No grades + print(student.add_grade(5)) -# Output: Grade added: 5 + +Grade added: 5 + print(student.add_grade(4)) -# Output: Grade added: 4 + +Grade added: 4 + print(student.add_grade(5)) -# Output: Grade added: 5 - -print(f"Average: {student.average_grade()}") -# Output: Average: 4.666666666666667 + +Grade added: 5 + + +print(f"Average grade: {student.average_grade()}") + +Average grade: 4.7 + ``` -The `add_grade` method modifies `self.grades` β€” the list stored in the object. Changes happen **in place**: the next call to `student.average_grade()` sees the updated state. It's not "return a new list", it's "modify the existing one". +The `add_grade` method changes `self.grades`, the list stored in the object. The changes happen **in place**: the next `student.average_grade()` call sees the updated state. It's not "return a new list", it's "change the existing one". ## Dynamic attributes -In Python, you can add **any** attribute to an object at any time, even one that wasn't declared in `__init__`: +Changing existing attributes is business as usual. Python allows more: you can add **any** new attribute to an object at any moment, even one not declared in `__init__`. Let's check on a slimmed-down `Student` that has only a name: -```python-executable +```python class Student: def __init__(self, name): self.name = name student = Student("Maria") -student.age = 19 # added a new attribute on the fly +student.age = 19 # added a new attribute on the fly student.favorite_color = "blue" print(student.age) -# Output: 19 + +19 + print(student.favorite_color) -# Output: blue + +blue + ``` -Technically this works, but in real code you almost never do this. A few reasons: +Technically this works, but real code almost never does it, and here is why: -- **The object's state becomes unpredictable.** Looking at the `Student` class, you can't tell what attributes an object actually has. -- **IDE and linter autocomplete won't help**: they only know what's declared in `__init__`. -- **A typo creates a new attribute instead of raising an error.** If you write `student.aeg = 19` instead of `student.age = 19`, Python silently creates a new `aeg` field, and the bug is hard to spot. +- **The object's state becomes unpredictable.** Looking at the `Student` class, you can't tell which attributes an object actually has. +- **IDEs and linters can't help** with autocompletion: they only know what's declared in `__init__`. +- **A typo silently creates a new attribute** instead of a clear error. Write `student.aeg = 19` instead of `student.age = 19`, and Python quietly creates a new `aeg` field β€” a hard bug to find. -The rule of thumb: declare all attributes in `__init__`, even with `None` if they'll be filled in later: +So declare all of an object's attributes in `__init__` β€” even with a `None` value if they get filled in later: ```python class Student: def __init__(self, name): self.name = name - self.age = None # will be filled in later + self.age = None # will be filled in later self.grades = [] ``` -That way the class honestly describes what fields an object has, and typos immediately become `AttributeError`. +That way the class honestly describes which fields an object has, and typos immediately turn into an `AttributeError`. -## What's next? +## Understanding check -In the next lesson we'll go deeper into attributes: the difference between instance and class attributes, and the classic mutable-default gotcha in `__init__`. +**What happens when `Person.greet(person)` runs, if `greet` is defined with `self` as the first parameter?** ---- +1. An error: you can't call a method through the class name. β€” Calling a method through the class name is allowed. You just pass the object as the first argument yourself. + +2. **Correct answer:** The same as person.greet() β€” person becomes self. β€” Python executes person.greet() as Person.greet(person): the object to the left of the dot is passed as the first argument itself and arrives in self. + +3. The method runs, but self is None. β€” self receives exactly what was passed as the first argument β€” in this case the person object. -**What happens when you call `Person.greet(person)` if `greet` is defined with `self` as its first parameter?** +4. A new Person object is created. β€” Objects are created only through Person() (the constructor call), not through a method call. +In the next article we'll look at attributes in detail: how instance attributes differ from class attributes, and how to avoid the classic trap with mutable default values in `__init__`. diff --git a/course/module-2/datetime-handling/article.md b/course/module-2/datetime-handling/article.md index f7691a9..2bebafc 100644 --- a/course/module-2/datetime-handling/article.md +++ b/course/module-2/datetime-handling/article.md @@ -1,3 +1,9 @@ +--- +meta: + title: "Working with Date and Time in Python" + description: "The datetime module: three classes, arithmetic with timedelta, strftime and strptime, ISO 8601 and zoneinfo time zones." +--- + # Working with Date and Time in Python Suppose we have a log file with lines like `2026-05-20 14:30:42 ERROR ...`, and we need to count how many errors happened in the last 24 hours. That means: parse the date string into an object, compute the difference from "now", and (if showing the user) return it in their time zone and a friendly format. @@ -8,46 +14,60 @@ These three operations β€” parsing a string, arithmetic, formatting β€” are the The `datetime` module has three main classes, and it matters which one you pick: -- **`datetime`**: a specific moment in time, down to microseconds. The right choice 90% of the time. -- **`date`**: just the date, no time. Birthdays, deadlines, order dates. -- **`time`**: just the time of day, no date. A meeting slot in a calendar. +- **`datetime`**: a specific moment in time, down to microseconds. The right choice 90% of the time. +- **`date`**: just the date, no time. Birthdays, deadlines, order dates. +- **`time`**: just the time of day, no date. A meeting slot in a calendar. -```python-executable +```python from datetime import datetime, date, time # A moment in time now = datetime.now() print(now) -# Output: 2026-05-20 14:30:25.123456 + +2026-05-20 14:30:25.123456 + # Just a date birthday = date(1990, 5, 15) print(birthday) -# Output: 1990-05-15 + +1990-05-15 + # Just a time of day meeting = time(14, 30) print(meeting) -# Output: 14:30:00 + +14:30:00 + ``` -All three have attributes for individual components: `.year`, `.month`, `.day`, `.hour`, `.minute`, `.second`, `.microsecond`. +Each class has attributes for its own components: -```python-executable +- `date` has `.year`, `.month`, `.day`; +- `time` has `.hour`, `.minute`, `.second`, `.microsecond`; +- `datetime`, which combines date and time, has all of them. + +```python from datetime import datetime now = datetime(2026, 5, 20, 14, 30) print(now.year, now.month, now.day) -# Output: 2026 5 20 + +2026 5 20 + print(now.weekday()) # 0 = Monday, 6 = Sunday -# Output: 2 + +2 + ``` ## Date arithmetic: timedelta You can add and subtract dates directly: the result is a `timedelta` (a duration), or a new `datetime`. -```python-executable +```python from datetime import datetime, timedelta now = datetime(2026, 5, 20, 14, 30) @@ -55,15 +75,21 @@ now = datetime(2026, 5, 20, 14, 30) # Add an interval week_later = now + timedelta(days=7) print(week_later) -# Output: 2026-05-27 14:30:00 + +2026-05-27 14:30:00 + # Difference between moments is a timedelta deadline = datetime(2026, 6, 1) delta = deadline - now print(delta) -# Output: 11 days, 9:30:00 + +11 days, 9:30:00 + print(delta.days, delta.total_seconds()) -# Output: 11 985800.0 + +11 984600.0 + ``` `timedelta` accepts `days`, `hours`, `minutes`, `seconds`, `weeks` β€” but **not** `months` or `years`, because their length varies (28 or 29 days in February, 365 or 366 in a year). For "add N months" the third-party library `dateutil` covers the gap. @@ -72,47 +98,73 @@ print(delta.days, delta.total_seconds()) In real code, dates constantly flow through strings β€” APIs, logs, databases. Mnemonic to remember which method does what: -- **`strftime`** β€” **f**ormat: object β†’ string -- **`strptime`** β€” **p**arse: string β†’ object +- **`strftime`** β€” **f**ormat: object β†’ string +- **`strptime`** β€” **p**arse: string β†’ object + +`"2026-05-20 14:30"` β†’ `strptime` β†’ datetime object β†’ `strftime` β†’ `"20.05.2026 14:30"`. -```python-executable +```python from datetime import datetime # Object β†’ string now = datetime(2026, 5, 20, 14, 30) print(now.strftime("%d.%m.%Y %H:%M")) -# Output: 20.05.2026 14:30 + +20.05.2026 14:30 + print(now.strftime("%A, %d %B %Y")) -# Output: Wednesday, 20 May 2026 + +Wednesday, 20 May 2026 + # String β†’ object parsed = datetime.strptime("20.05.2026 14:30", "%d.%m.%Y %H:%M") print(parsed) -# Output: 2026-05-20 14:30:00 + +2026-05-20 14:30:00 + ``` The format is described by a string with directives like `%Y`, `%m`, `%d`: -| Directive | Description | Example | -| --------- | ------------------------ | ------- | -| `%Y` | Year, 4 digits | 2026 | -| `%y` | Year, 2 digits | 26 | -| `%m` | Month (01-12) | 05 | -| `%B` | Full month name | January | -| `%b` | Abbreviated month name | Jan | -| `%d` | Day of month (01-31) | 20 | -| `%A` | Full weekday name | Wednesday | -| `%a` | Abbreviated weekday name | Wed | -| `%H` | Hour (00-23) | 14 | -| `%I` | Hour (01-12) | 02 | -| `%M` | Minutes (00-59) | 30 | -| `%S` | Seconds (00-59) | 25 | +| Directive | Description | Example | +| --------- | -------------------- | --------- | +| `%Y` | Year, 4 digits | 2026 | +| `%m` | Month (01-12) | 05 | +| `%B` | Full month name | May | +| `%d` | Day of month (01-31) | 20 | +| `%A` | Full weekday name | Wednesday | +| `%H` | Hour (00-23) | 14 | +| `%M` | Minutes (00-59) | 30 | +| `%S` | Seconds (00-59) | 42 | + +Now let's assemble the example from the introduction: take a log line, turn the date into an object, and check whether the error happened within the last 24 hours: + +```python +from datetime import datetime, timedelta + +line = "2026-05-20 14:30:42 ERROR disk full" + +logged_at = datetime.strptime(line[:19], "%Y-%m-%d %H:%M:%S") +now = datetime(2026, 5, 21, 10, 0) + +print(now - logged_at) + +19:29:18 + +print(now - logged_at < timedelta(days=1)) + +True + +``` + +The slice `line[:19]` cuts off the first 19 characters β€” exactly the date and time, without the message tail. ## ISO 8601: the date interchange standard When a date crosses a system boundary (API, JSON, database), use **ISO 8601**: `2026-05-20T14:30:00`. `datetime` has built-in methods for this format, and they're faster and more robust than `strftime`/`strptime`: -```python-executable +```python from datetime import datetime now = datetime(2026, 5, 20, 14, 30) @@ -120,12 +172,16 @@ now = datetime(2026, 5, 20, 14, 30) # To ISO string iso_string = now.isoformat() print(iso_string) -# Output: 2026-05-20T14:30:00 + +2026-05-20T14:30:00 + # And back parsed = datetime.fromisoformat("2026-05-20T14:30:00") print(parsed) -# Output: 2026-05-20 14:30:00 + +2026-05-20 14:30:00 + ``` Rule of thumb: inside your program keep dates as `datetime` objects; when crossing the boundary (out to JSON or a DB) use `.isoformat()`; when reading from outside, use `fromisoformat()`. A custom `strftime` format is only needed when you're showing dates to a human. @@ -134,9 +190,9 @@ Rule of thumb: inside your program keep dates as `datetime` objects; when crossi `datetime.now()` with no arguments returns a "naive" datetime β€” it has no time zone information. Common trap: the program works fine on your laptop, then runs on a server in a different country and silently shows times 7 hours off. -The right thing is to work with **aware** datetimes that carry a time zone. Since Python 3.9 there's a built-in `zoneinfo` module that knows real-world time zones, including daylight saving: +The right thing is to work with **aware** datetimes β€” ones that carry a time zone. Since Python 3.9 there's a built-in `zoneinfo` module that knows real-world time zones, including daylight saving: -```python-executable +```python from datetime import datetime from zoneinfo import ZoneInfo @@ -145,9 +201,13 @@ utc_now = datetime(2026, 5, 20, 14, 30, tzinfo=ZoneInfo("UTC")) moscow_now = utc_now.astimezone(ZoneInfo("Europe/Moscow")) print(utc_now) -# Output: 2026-05-20 14:30:00+00:00 + +2026-05-20 14:30:00+00:00 + print(moscow_now) -# Output: 2026-05-20 17:30:00+03:00 + +2026-05-20 17:30:00+03:00 + ``` Zone names are standardized (IANA tz database): `"Europe/Moscow"`, `"America/New_York"`, `"Asia/Tokyo"`. The typical practice when storing dates in a database: always store in UTC, convert to the user's local zone only when displaying. @@ -158,29 +218,39 @@ Zone names are standardized (IANA tz database): `"Europe/Moscow"`, `"America/New Beyond `datetime`, there's a lower-level `time` module. Two of its functions come up often: -```python-executable +```python import time # Current moment as a Unix timestamp (seconds since January 1, 1970) print(time.time()) -# Output: 1779373825.123456 + +1779373825.123456 + # Pause for N seconds print("Start") time.sleep(0.1) print("0.1 seconds passed") -# Output: -# Start -# 0.1 seconds passed + +Start +0.1 seconds passed + ``` -`time.time()` returns a Unix timestamp β€” a single number, time-zone-independent, common in databases and logs. Convert it back to a `datetime` via `datetime.fromtimestamp(ts, tz=ZoneInfo("UTC"))`. +`time.time()` returns a Unix timestamp β€” a single number, time-zone-independent, common in databases and logs. The `fromtimestamp` method converts it back to a `datetime` β€” you pass it the number and a time zone. -## What's next? +## Understanding check -`datetime` will keep coming up: working with APIs (dates arrive as ISO 8601 strings), databases (storing event times), logging. The main rule: inside your program keep dates as objects; convert to strings only at the boundary with the outside world. +**Which class from the datetime module represents a duration of time?** ---- +1. datetime β€” The datetime class represents a specific moment, not a duration. -**Which class from the datetime module represents a duration of time?** +2. date β€” The date class represents only a date, without time. + +3. time β€” The time class represents only a time of day, without a date. + +4. **Correct answer:** timedelta β€” timedelta represents a duration and is used for date arithmetic: the difference between two datetimes is a timedelta, and you can add a timedelta to a datetime to get a new moment. + +`datetime` will keep coming up: working with APIs (dates arrive as ISO 8601 strings), databases (storing event times), logging. The main rule: inside your program keep dates as objects; convert to strings only at the boundary with the outside world. +In the next lesson β€” the final chapter of the module: type annotations, hints about what values a function takes and returns. diff --git a/course/module-2/decorators/article.md b/course/module-2/decorators/article.md index 5a67e4f..9216d5e 100644 --- a/course/module-2/decorators/article.md +++ b/course/module-2/decorators/article.md @@ -1,3 +1,9 @@ +--- +meta: + title: "Decorators in Python" + description: "How decorators work: a wrapper around a function, *args and **kwargs, functools.wraps, and decorators with parameters." +--- + # Decorators in Python Suppose we have several functions and we want to measure how long each one takes. We could copy the timing code into every function: @@ -7,24 +13,41 @@ def slow_function(): start = time.time() # ... main work elapsed = time.time() - start - print(f"slow_function: {elapsed:.4f} s") + print(f"slow_function: {round(elapsed, 4)} s") def another_function(): start = time.time() # ... main work elapsed = time.time() - start - print(f"another_function: {elapsed:.4f} s") + print(f"another_function: {round(elapsed, 4)} s") ``` It works, but next week we'll want to change the log format and we'll have to edit **every** function in turn. Plus the actual logic gets buried under boilerplate. Decorators solve exactly this: you write the wrapper once and stick it on any function with a short `@wrapper` line above its definition. +## A function is a value + +One stepping stone first β€” decorators make no sense without it. A function in Python is a value like a number or a string: you can put it in a variable, pass it to another function and return it from a function. + +```python +def say_hello(): + print("Hello!") + +greet = say_hello # no parentheses: we take the function itself, not its result +greet() + +Hello! + +``` + +`say_hello` without parentheses is the function itself; with parentheses it's a call. You already passed a function around in the previous lesson: `sorted(key=lambda ...)` is a function handed to another function. One thing remains: a function can be declared inside another one and returned β€” and that's exactly how a decorator works. + ## Basic syntax A decorator is a function that takes another function and returns its "wrapped" version (with added behavior): -```python-executable +```python def my_decorator(func): def wrapper(): print("Before the call") @@ -37,25 +60,34 @@ def say_hello(): print("Hello, world!") say_hello() -# Output: -# Before the call -# Hello, world! -# After the call + +Before the call +Hello, world! +After the call + ``` -Writing `@my_decorator` above `say_hello` is syntactic sugar for this equivalent: +Writing `@my_decorator` above `say_hello` is a shorthand for this line: ```python say_hello = my_decorator(say_hello) ``` -That is, we reassign `say_hello` to the new function the decorator returned. No magic β€” just reassignment. +That is, the name `say_hello` now points to the new function the decorator returned β€” an ordinary reassignment. + +`say_hello()` β†’ `@my_decorator` β†’ `wrapper()`: + +1. `print("Before the call")` +2. `say_hello()` +3. `print("After the call")` + +The decorator returned a new function: the original inside, plus code before and after ## Decorator with function arguments If the wrapped function takes arguments, the wrapper has to forward them. The universal trick is `*args, **kwargs`: -```python-executable +```python def my_decorator(func): def wrapper(*args, **kwargs): print("Before") @@ -69,19 +101,22 @@ def add(a, b): return a + b print(add(5, 3)) -# Output: -# Before -# After -# 8 + +Before +After +8 + ``` `*args, **kwargs` means "accept any positional and keyword arguments", and `func(*args, **kwargs)` forwards them through. This trick makes the decorator universal β€” it works with any function. +Notice the output order: `8` prints last, because the outer `print` waits for the wrapper to finish and return the result. + ## A practical decorator: timing -The very timing decorator we started with: +The very timing decorator we started with. One detail: every function has a `__name__` attribute holding its name β€” the wrapper uses it to label the measurement: -```python-executable +```python import time def timing(func): @@ -89,7 +124,7 @@ def timing(func): start = time.time() result = func(*args, **kwargs) elapsed = time.time() - start - print(f"{func.__name__}: {elapsed:.4f} s") + print(f"{func.__name__}: {round(elapsed, 4)} s") return result return wrapper @@ -98,16 +133,18 @@ def calculate_sum(n): return sum(range(n)) calculate_sum(1_000_000) -# Output: calculate_sum: 0.0462 s + +calculate_sum: 0.0462 s + ``` Now adding timing to any function is one line `@timing` on top. Want to change the log format? Edit the single `timing` function, not every function in the project. -## functools.wraps: preserving name and docstring +## functools.wraps: preserving the function name -A naive decorator has a quiet side effect: the wrapped function "loses" its name and documentation, because from the outside you see the `wrapper`, not the original: +A naive decorator has a quiet side effect: the wrapped function "loses" its name, because from the outside you see the `wrapper`, not the original: -```python-executable +```python def timing(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) @@ -115,18 +152,17 @@ def timing(func): @timing def calculate_sum(n): - """Sums numbers from 0 to n.""" return sum(range(n)) print(calculate_sum.__name__) -# Output: wrapper -print(calculate_sum.__doc__) -# Output: None + +wrapper + ``` -In real code this breaks debugging, logging, and IDE introspection. It's fixed by one line β€” the `@functools.wraps(func)` decorator on `wrapper`: +In real code this breaks debugging and error messages. It's fixed by one line β€” the `@functools.wraps(func)` decorator on `wrapper`: -```python-executable +```python from functools import wraps def timing(func): @@ -137,22 +173,21 @@ def timing(func): @timing def calculate_sum(n): - """Sums numbers from 0 to n.""" return sum(range(n)) print(calculate_sum.__name__) -# Output: calculate_sum -print(calculate_sum.__doc__) -# Output: Sums numbers from 0 to n. + +calculate_sum + ``` -Rule of thumb: writing your own decorator? Always wrap the inner function with `@wraps(func)`. It's free and preserves introspection. +Rule of thumb: writing your own decorator β€” always wrap the inner function with `@wraps(func)`. ## Decorator with parameters Sometimes you want to pass options to the decorator itself, e.g. "repeat the call N times". This needs another level: an outer function takes the parameter and returns the "real" decorator: -```python-executable +```python from functools import wraps def repeat(n=1): @@ -171,10 +206,11 @@ def say_hi(name): print(f"Hi, {name}!") say_hi("Anna") -# Output: -# Hi, Anna! -# Hi, Anna! -# Hi, Anna! + +Hi, Anna! +Hi, Anna! +Hi, Anna! + ``` Three levels of nesting looks scary, but the logic is simple: @@ -183,41 +219,21 @@ Three levels of nesting looks scary, but the logic is simple: 2. `decorator(func)` takes the function and returns a wrapper. 3. `wrapper(*args, **kwargs)` handles the actual call. -## Chaining decorators - -You can apply more than one decorator. They apply **bottom-up**: the one closest to the function goes first: - -```python-executable -def bold(func): - def wrapper(*args, **kwargs): - return f"{func(*args, **kwargs)}" - return wrapper - -def italic(func): - def wrapper(*args, **kwargs): - return f"{func(*args, **kwargs)}" - return wrapper +Written out, it's the same trick as before, with one extra call: -@bold -@italic -def format_text(text): - return text - -print(format_text("Hello, world!")) -# Output: Hello, world! +```python +say_hi = repeat(n=3)(say_hi) ``` -`@italic` applies to `format_text` first β€” that gives an "italic function". Then `@bold` wraps it from outside β€” giving `bold(italic(format_text))`. So `` closes first, then ``. +First the ordinary call `repeat(n=3)` runs β€” it returns the actual decorator, which is applied to `say_hi` right away. -## You've already met decorators - -Back in encapsulation we used `@property` and `@balance.setter` β€” those are decorators from the standard library. `@property` takes a getter function and turns it into a "computed attribute". No magic: the same mechanism we're discussing now, applied in the context of classes. +The name `_` in the loop is a convention: the variable exists only to make the loop run, its value is never used. ## Where decorators live in the real world A few places you'll meet them most often: -**Web frameworks (Flask, FastAPI, Django).** Wiring a URL to a handler function: +**Web frameworks.** Wiring a page address to a handler function β€” in Flask or FastAPI, for example: ```python @app.route('/home') @@ -225,41 +241,24 @@ def home(): return "Home page" ``` -`@app.route` registers the function in the framework's router β€” without the decorator you'd have to call `app.add_url_rule(...)` for every endpoint manually. - -**Caching.** Storing results so we don't recompute the same thing: +`@app.route` registers the function in the framework's router: the browser requests `/home` β€” the framework calls `home()`. -```python-executable -from functools import wraps - -def memoize(func): - cache = {} - @wraps(func) - def wrapper(*args): - if args not in cache: - cache[args] = func(*args) - return cache[args] - return wrapper +**Caching.** The `@lru_cache` decorator from `functools` remembers a function's results: a repeated call with the same arguments recomputes nothing and returns the ready answer right away. -@memoize -def fib(n): - if n < 2: - return n - return fib(n - 1) + fib(n - 2) +**Tests.** In pytest, the decorators `@pytest.fixture` and `@pytest.mark.parametrize` turn a regular function into a data setup or a whole series of tests β€” you'll meet them in the third module of the course. -print(fib(30)) -# Output: 832040 -``` +## Understanding check -Without `@memoize`, `fib(30)` would recompute the same calls millions of times and stall. With the cache β€” instantaneous. The standard library has this built in: `from functools import lru_cache`. +**What is the primary purpose of decorators in Python?** -**Tests.** In pytest, `@pytest.fixture` and `@pytest.mark.parametrize` are decorators that turn a regular function into a fixture or a parametrized test. +1. **Correct answer:** To modify or extend functions without changing their source code β€” A decorator wraps a function to add behavior before, after, or around its call without touching the function itself. -## What's next? +2. To create new variables and data structures β€” Decorators work with functions, not with creating variables or data structures. -Decorators are a workhorse of Python: libraries and frameworks are built on them (Flask routes, pytest fixtures, dataclasses, type checkers). Once you see that `@something` is just `func = something(func)`, the "magic" of these libraries disappears β€” it's all function composition underneath. +3. To optimize Python code for faster execution β€” Decorators can be used for optimization (e.g. caching via @lru_cache), but that is not their primary purpose. ---- +4. To document code and generate API documentation β€” Decorators are sometimes used to attach metadata, but their main job is functional, not documentary. -**What is the primary purpose of decorators in Python?** +Decorators are a workhorse of Python: web frameworks, tests and the `@property` you already know are all built on them. Once you know that `@something` is just `func = something(func)`, such code becomes much easier to read. +In the next lesson β€” working with dates and times: the `datetime` module, date arithmetic and formatting. diff --git a/course/module-2/encapsulation/article.md b/course/module-2/encapsulation/article.md index 6865d43..8db978a 100644 --- a/course/module-2/encapsulation/article.md +++ b/course/module-2/encapsulation/article.md @@ -1,26 +1,46 @@ +--- +meta: + title: "Encapsulation" + description: "How a class protects its data: the single-underscore convention, @property, and read-only attributes." +--- + # Encapsulation in Python -Suppose we're writing a bank account class: +Let's build a piggy-bank class: `deposit` adds money and refuses a negative amount. The check is there β€” but getting around it is trivial: ```python class BankAccount: def __init__(self, balance): self.balance = balance + def deposit(self, amount): + if amount > 0: + self.balance += amount + account = BankAccount(1000) -print(account.balance) # 1000 -account.balance = -1_000_000 # oops + +account.deposit(-500) # the check won't let this through +print(account.balance) + +1000 + + +account.balance = -1_000_000 # but this β€” go right ahead +print(account.balance) + +-1000000 + ``` -The class doesn't object: `balance` is a public attribute, and you can assign anything to it β€” including things that should never be possible. The class fails to enforce its **invariants** (rules that must always hold, for example "the balance is non-negative"). +The check in `deposit` hasn't gone anywhere β€” we simply bypassed it: `balance` is a regular attribute, and you can assign anything to it. The class cannot enforce its own rules β€” for example, "the balance is never negative". -Encapsulation is the idea that an object has a **public interface** (how the outside world talks to it) and **internal state** (which the outside world shouldn't poke). External code calls methods; the class itself watches over its data and keeps it valid. +Encapsulation is the answer to this problem: an object has a **public interface** (how the outside world talks to it) and **internal state** (which the outside world shouldn't poke). External code calls methods; the class itself makes sure its data stays correct. ## The single-underscore convention -Python has no `private` keyword. Instead there's a convention: an attribute or method whose name starts with an underscore is considered "internal", meaning don't touch from outside: +How do you tell internal from public? Python has a convention for that: an attribute or method whose name starts with an underscore is considered "internal" β€” don't touch it from outside: -```python-executable +```python class BankAccount: def __init__(self, balance): self._balance = balance # underscore = "internal" @@ -40,18 +60,24 @@ account = BankAccount(1000) account.deposit(500) account.withdraw(2000) # more than the balance, ignored print(account.get_balance()) -# Output: 1500 + +1500 + ``` From the outside only `deposit`, `withdraw`, and `get_balance` are available: everything you need to work with the account. You **technically can** still poke `_balance` from outside (Python doesn't forbid it), but the convention says: don't, otherwise you're bypassing the class's checks. The Python community states this philosophy as **"we're all consenting adults here"**. The language doesn't forbid β€” it signals that you shouldn't touch this. Responsibility is on the programmer. +outside code β†’ through methods β†’ `deposit()`, `withdraw()`, `get_balance()`; directly β†’ `_balance`. The account is used through its methods, while `_balance` is changed only by the class itself. + ## Properties: an attribute on the outside, a method on the inside -Often you want `account.balance` to **look** like a regular attribute from the outside, while underneath there's actually a method (for example, with validation). In Java you'd write `getBalance()` and `setBalance()`. In Python there's `@property`: +Often you want `account.balance` to **look** like a regular attribute from the outside, while underneath there's actually a method with validation. That's what `@property` is for. -```python-executable +The line with `@` above a method is a **decorator**: a device that changes how the method behaves. Decorators get their own lesson later in the course; for now it's enough to know what these two do: + +```python class Account: def __init__(self, balance): self._balance = balance @@ -70,81 +96,77 @@ account = Account(1000) # Used like a regular attribute: print(account.balance) -# Output: 1000 + +1000 + account.balance = 500 # triggers the setter with validation print(account.balance) -# Output: 500 + +500 + # Trying to set a negative value: try: account.balance = -100 except ValueError as e: print(f"Error: {e}") -# Output: Error: Balance cannot be negative + +Error: Balance cannot be negative + ``` -The `@property` decorator turns a method into a "computed attribute". `@balance.setter` defines what happens on assignment. From the outside it all looks like `account.balance = 500`, but inside the class the validation kicks in. +`@property` turns a method into a "computed attribute": from the outside, `account.balance` is read without parentheses. `@balance.setter` defines what happens on assignment: from the outside it looks like a plain assignment, but inside the class the validation kicks in. -> In Java and C++ people often write `get_x()` and `set_x()` methods. In Python that's not idiomatic: use `@property`. +The example uses `raise` and `try/except` β€” that's the error mechanism: `raise` aborts the operation and signals that a value is invalid, while `try/except` catches it in the calling code. They have their own lesson later in the course; here it's enough to see that the check in the setter prevents an invalid value from being stored. ## Read-only properties If a `@property` has no setter, the attribute becomes read-only. This is handy for **computed** values that don't make sense to "set" from outside: -```python-executable +```python import math class Circle: def __init__(self, radius): - self._radius = radius - - @property - def radius(self): - return self._radius - - @radius.setter - def radius(self, value): - if value <= 0: - raise ValueError("Radius must be positive") - self._radius = value + self.radius = radius @property def area(self): - return math.pi * self._radius ** 2 + return math.pi * self.radius ** 2 circle = Circle(5) -print(f"Radius: {circle.radius}, area: {circle.area:.2f}") -# Output: Radius: 5, area: 78.54 - -# Change the radius, the area is recomputed automatically -circle.radius = 7 -print(f"Radius: {circle.radius}, area: {circle.area:.2f}") -# Output: Radius: 7, area: 153.94 +print(f"Radius: {circle.radius}, area: {round(circle.area, 2)}") + +Radius: 5, area: 78.54 + # You can't assign to area (there's no setter) try: circle.area = 100 except AttributeError as e: print(f"Error: {e}") -# Output: Error: property 'area' of 'Circle' object has no setter + +Error: property 'area' of 'Circle' object has no setter + ``` -`area` always returns the current value, and you can't assign to it: there shouldn't be a "set the area" operation, since it follows from the radius. +`area` always returns the current value, and you can't assign to it β€” rightly so: the area isn't stored separately, it follows from the radius. -## What about double underscores? +## What encapsulation gives you -You may sometimes see attributes with two leading underscores: `self.__balance`. That triggers Python's **name mangling**: the attribute is renamed inside the object to `_ClassName__balance`. It's useful in rare cases (for example, to make sure an attribute doesn't collide with a same-named attribute in a subclass) and is almost never seen in regular code. By default, use a single underscore. +The main point: the class becomes **responsible for its own data**. From outside there's no way (by convention) to bypass its checks and leave the object with incorrect data. If later you need to change how the data is stored (say, `_balance` becomes a dict with a transaction history), the outside code doesn't break, because it still talks to the class through the same public interface β€” `deposit`, `withdraw`, `balance`. -## What encapsulation gives you +## Understanding check -The main point: the class becomes **responsible for its own data**. From outside, there's no (by convention) way to bypass its checks and leave the object in an invalid state. If later you need to change how the data is stored (say, `_balance` becomes a dict with a transaction history), the outside code doesn't break, because it still talks to the class through the same interface β€” `deposit`, `withdraw`, `balance`. +**How do you set up an attribute that can be read from outside but not changed?** -## What's next? +1. **Correct answer:** @property without a setter β€” From the outside such an attribute reads like a regular one, and Python raises AttributeError when you try to assign to it. -In the next lesson we'll take on the third principle of OOP β€” polymorphism: how a single interface can work with objects of different classes, and why that makes code simpler. +2. A regular attribute without an underscore β€” A regular attribute can be changed from anywhere in the program β€” there is no write protection. ---- +3. An attribute with a single underscore β€” A single underscore is only an "internal, do not touch" signal: technically it can still be changed from outside. -**Which access level is most appropriate for an attribute that should be readable but should not be changed outside the class?** +4. A get_x() method and an \_x attribute β€” Reading would require a method call with parentheses, and \_x itself stays changeable. In Python, @property is used instead. +In the next lesson we'll take on the third principle of OOP β€” polymorphism: how a single interface can work with objects of different classes, and why that makes code simpler. diff --git a/course/module-2/exception-handling/article.md b/course/module-2/exception-handling/article.md index 86dc343..e3e9086 100644 --- a/course/module-2/exception-handling/article.md +++ b/course/module-2/exception-handling/article.md @@ -1,264 +1,184 @@ -# Exception Handling in Python - -Traveling through unknown territory requires careful preparation β€” an experienced explorer thinks through possible dangers in advance, checks the map, prepares equipment, and plans the route. In programming, exception handling works in a similar way β€” it's a reliable tool that allows a program to properly respond to unexpected situations and continue working, instead of crashing at the first error. 🧭 - -## What are exceptions? +--- +meta: + title: "Exception Handling in Python" + description: "How to catch errors with try/except, what else and finally are for, and why you should not catch every exception at once." +--- -> Exceptions are events that disrupt the normal flow of program execution, which can be handled by special language constructs. +# Exception Handling in Python -## Why do we need exception handling? +A program asks the user for their age and adds a year: -Exception handling allows you to: +```python +age = int(input("How old are you? ")) +print("Next year you'll be", age + 1) +``` -1. **Prevent program crashes** β€” your program will continue running even if something goes wrong -2. **Separate error handling code from the main logic** β€” makes code more readable and understandable -3. **Handle different types of errors centrally** β€” simplifies program structure -4. **Make your program more robust** β€” it can work correctly even in unexpected situations +As long as they type a number, everything works. But the moment the user types "forty" instead of `40`, `int()` can't turn it into a number: the program crashes with a `ValueError`, and the `print` line never runs. One bad input takes down the whole program. -## Try-except blocks +Exception handling is for exactly these moments: catch the error, respond, and keep going, instead of crashing at the first surprise. We've postponed this conversation twice β€” in the lessons on text files and encapsulation. The time has come. -The main mechanism for exception handling in Python is the `try-except` block. It allows you to: +> An exception is an error that occurs while the program is running. By default it stops the program, but it can be caught and handled. -- Isolate potentially dangerous code -- Catch errors -- Perform alternative actions -- Continue program execution +## The try-except block -### Try-except block syntax +The risky code goes into `try`, the reaction to the error into `except`: ```python try: - # Risky code that might raise an exception + # code that might fail except ExceptionType: - # Code that will execute when the specified exception occurs + # what to do if it failed ``` -Here's a simple example: +Here's what it looks like with a real error: ```python try: - # Code that might raise an exception - result = 10 / 0 # Division by zero will raise ZeroDivisionError + result = 10 / 0 except ZeroDivisionError: - # Code that will execute when the exception occurs print("Error: division by zero!") + +Error: division by zero! + ``` -### Complete try-except block structure - -The complete structure of a try-except block includes additional sections: - -```python -try: - # Risky code -except ExceptionType1: - # Handle exception type 1 -except ExceptionType2: - # Handle exception type 2 -else: - # Executes if no exceptions occurred in the try block -finally: - # Always executes, regardless of whether exceptions occurred -``` - -## Handling different types of exceptions +The program didn't crash: Python reached the division, saw the error, jumped into `except` and kept going. -### Sequential handling of different exceptions +## Different errors β€” different reactions -You can handle different types of exceptions differently: +Different things can fail inside one `try`, and each exception type gets its own `except`. Python checks them in order and runs the first one that matches: ```python +file_name = "data.txt" + try: - # Several operations that might raise different exceptions - file_name = input("Enter file name: ") # Let's assume "data.txt" was entered file = open(file_name, "r") - line = file.readline() - number = int(line.strip()) - result = 100 / number - print(f"Result: {result}") + number = int(file.readline().strip()) + print(f"Result: {100 / number}") except FileNotFoundError: print(f"File {file_name} not found") except ValueError: - print("Cannot convert data to a number") + print("The file doesn't contain a number") except ZeroDivisionError: - print("Error: division by zero") -except Exception as e: - print(f"An unknown error occurred: {e}") + print("The file contains zero β€” can't divide") + +File data.txt not found + ``` -Important: Python checks the `except` blocks in the order they are declared and executes the first matching block. - -### Handling multiple exceptions with one block - -If you need the same handling for different exceptions: +If the reaction to different errors is the same, the types are listed in parentheses, separated by commas: ```python try: value = int("abc") - result = 10 / 0 except (ValueError, ZeroDivisionError): - print("A calculation error occurred") + print("An error occurred in the calculations") + +An error occurred in the calculations + ``` -## Else and finally blocks - -### Else block +## The else and finally blocks -The `else` block executes only if no exceptions were raised in the `try` block: +`else` runs only if there were no errors in `try`: ```python try: - number = int("42") # This will execute successfully + number = int("42") except ValueError: - print("This is not a number") + print("That's not a number") else: - # This block will execute since no exception occurred - print(f"Success! Number: {number}") - print(f"Square of the number: {number ** 2}") + print(f"Success! The number: {number}") + +Success! The number: 42 + ``` -The `else` block is useful when you need to execute code only after a risky operation has completed successfully. - -### Finally block - -The `finally` block always executes, regardless of whether an exception occurred or not: +`finally` runs always β€” error or no error: ```python +f = open("example.txt", "w") try: - f = open("example.txt", "w") f.write("Hello, world!") - # Let's assume an exception might occur here -except IOError: - print("An I/O error occurred") finally: - print("Closing file") - f.close() # The file will be closed in any case + f.close() # runs even if the write above fails + print("File closed") + +File closed + ``` -The `finally` block is typically used for: +Notice that `open()` sits before `try`. Put it inside, and if the file fails to open, the variable `f` simply won't exist β€” and `f.close()` in `finally` will fail on its own. This is exactly the chore that the `with` statement from the files chapter automates: it closes the file for you. -- Releasing resources (closing files, database connections) -- Cleaning up temporary data -- Logging the completion of an operation +`finally` is for whatever must happen no matter what β€” most often releasing resources like open files. -## Getting information about an exception +## Information about the error: as -Use `as` to get the exception object: +With `as` you can get the exception object itself β€” for example, to show its message: ```python try: result = 10 / 0 except ZeroDivisionError as e: - print(f"Exception type: {type(e).__name__}") print(f"Message: {e}") + +Message: division by zero + ``` -## Raising exceptions +## Your own errors: raise -You can raise exceptions yourself using the `raise` statement: +The `raise` statement raises an exception manually. You've already seen it in the encapsulation lesson: the setter refused to store a negative balance. Here's how it works: ```python def check_age(age): if age < 0: raise ValueError("Age cannot be negative") - if age < 18: - print("You are a minor") - else: - print("You are an adult") + print(f"Age {age} accepted") try: check_age(-5) except ValueError as e: print(f"Error: {e}") + +Error: Age cannot be negative + ``` -## Creating custom exceptions - -You can create your own exception classes by inheriting from standard ones: - -```python -# Creating a custom exception class -class InvalidEmailError(Exception): - """Raised when an email doesn't match the format""" - pass - -def validate_email(email): - if "@" not in email: - raise InvalidEmailError("Email must contain the @ symbol") - print(f"Email {email} is valid") - -try: - validate_email("user.example.com") -except InvalidEmailError as e: - print(f"Validation error: {e}") -``` - -## Practical recommendations +## Don't catch everything at once -### 1. Specify concrete exceptions +A bare `except:` with no type catches any error β€” including ones you never suspected, like a typo in a variable name. The program "works" while the real problem stays silent: ```python -# Bad: +# Bad: no idea what actually happened try: number = int("abc") -except: # Catches all exceptions - print("Error") +except: + print("Some error") + +Some error + -# Good: +# Good: catch exactly what you expect try: number = int("abc") except ValueError: print("Invalid number format") + +Invalid number format + ``` -### 2. Minimize code in the try block - -```python -# Good: -try: - file = open("data.txt", "r") -except FileNotFoundError: - print("File not found") - file = None - -if file: - try: - content = file.read() - except: - print("Error reading file") - finally: - file.close() -``` +## Understanding check -### 3. Use else and finally correctly +**Which block always runs, whether an exception occurred or not?** -```python -def get_value_from_list(my_list, index): - try: - value = my_list[index] - except IndexError: - print(f"Index {index} out of range") - return None - else: - # This code executes only if no exception occurred - print(f"Value successfully retrieved") - return value - finally: - # This code always executes - print("List access operation completed") - -result = get_value_from_list([1, 2, 3], 1) -print(f"Result: {result}") - -result = get_value_from_list([1, 2, 3], 10) -print(f"Result: {result}") -``` - -## Understanding check +1. try β€” The try block contains code that may raise an exception, but it does not guarantee that all the code inside runs if an exception occurs. -**Which block always executes, regardless of whether an exception occurred or not?** +2. except β€” The except block runs only when an exception of the matching type occurs. +3. else β€” The else block runs only if no exception occurred in the try block. -## Conclusion +4. **Correct answer:** finally β€” The finally block always runs, exception or not. That is why it holds resource cleanup: closing files and database connections β€” things that must happen in any case. -Exception handling is your reliable protector against chaos in code. It turns potential disasters into controlled situations, allowing your programs to elegantly handle unexpected events. By properly using try-except, else, and finally, you create more reliable and robust code. πŸ›‘οΈ +In the next lesson β€” decorators. You've already met them twice: `@property` in encapsulation and `@abstractmethod` in polymorphism. It's time to see how the line with `@` works on the inside. diff --git a/course/module-2/files/article.md b/course/module-2/files/article.md index d4a2a72..fc1917d 100644 --- a/course/module-2/files/article.md +++ b/course/module-2/files/article.md @@ -1,22 +1,12 @@ -# Working with Files in Python - -Now we'll cover one of the most fundamental capabilities in programming β€” working -with files. -The ability to correctly read data from files and write it back -will be useful in virtually any project. πŸ“ - -Files are containers for storing data on disk. -Python provides powerful and convenient tools for working with them. Let's figure out how to use these tools! - -## Basic File Operations +--- +meta: + title: "Working with Files in Python" + description: "How to open, read, write, and close files in Python. Basic concepts and methods for working with the file system." +--- -In Python, working with files usually includes the following steps: - -1. **Opening a file** β€” specify the file and the mode of working with it -2. **Reading or writing** β€” perform the necessary operations with the file -3. **Closing the file** β€” free up system resources +# Working with Files in Python -Let's consider each of these steps in more detail. +Any variable lives only while the program is running: close it, and your to-do list, the score you racked up, your saved settings are gone. To make data survive a restart, you write it to a file on disk and read it back next time. That's what notes, game saves, report exports (almost any application) rest on. ## Opening Files @@ -25,94 +15,159 @@ It takes at least two parameters: the path to the file and the opening mode. > The `open(file, mode)` function returns a file object that can be used for reading, writing, or other operations with the file. -Here are the basic file opening modes: +The mode is a short string that answers "what are we going to do with this file". You pick one of three: -| Mode | Description | -| ----- | ---------------------------------------------------------- | -| `'r'` | Reading (default mode) | -| `'w'` | Writing (creates a new file or overwrites an existing one) | -| `'a'` | Appending β€” adds data to the end of the file | -| `'b'` | Binary mode (e.g., `'rb'` for reading a binary file) | -| `'t'` | Text mode (default mode) | -| `'+'` | Updating (reading and writing) | +| Letter | What it does | +| ------ | ----------------------------------------------------------- | +| `'r'` | read; the file must already exist (the default mode) | +| `'w'` | write from scratch: creates the file, wipes an existing one | +| `'a'` | append to the end; the old contents stay where they are | -Examples of opening files in different modes: +Let's see the two most common modes in action: ```python -# Opening a file for reading (default mode - 'r') -file = open('example.txt', 'r') -print(f"File opened in mode: {file.mode}") +# 'w' creates the file (or overwrites it) and opens it for writing +file = open('notes.txt', 'w') +file.write("Buy milk") file.close() -# Opening a file for writing -file = open('new_file.txt', 'w') -print(f"File opened in mode: {file.mode}") +# 'r' opens an existing file for reading +file = open('notes.txt', 'r') +print(file.read()) + +Buy milk + file.close() +``` -# Opening a binary file for reading -file = open('image.jpg', 'rb') -print(f"File opened in mode: {file.mode}") +`'w'` deserves a separate warning: it wipes the old contents silently, with no confirmation. Open a file full of important data with `'w'` instead of `'a'`, and the data is gone. `'r'` is the opposite: it requires the file to already exist, and if it doesn't, Python stops the program with a `FileNotFoundError`. + +## The with Context Manager + +In the example above, `close()` was called after the work with the file was done. Let's see why that call is needed and why real code almost never writes it. + +An open file has to be closed: while it's open it holds system resources, and written data may not reach the disk until it's closed. You can close it manually: + +```python +file = open('example.txt', 'w') +file.write("Example text") file.close() ``` +The catch is that it's easy to forget `close()`. And if something goes wrong between opening and closing and the program is interrupted, `close()` is never reached, and the file stays open. + +The `with` construct takes this off your hands: it closes the file when the block ends, no matter the outcome. + +```python +with open('example.txt', 'w') as file: + file.write("Example text") +# the file is already closed here automatically +print("File automatically closed after the with block") + +File automatically closed after the with block + +``` + +> The `with` context manager guarantees the file is closed even if an error occurs inside the block. It's the standard way to work with files in Python, and it's what we use from here on. + ## Reading from a File -After opening a file, its contents can be read in several ways: +After opening a file, its contents can be read in several ways. + +In all the examples below, a `sample.txt` file with three lines sits next to the program. ### Reading the entire file -```python -# Let's create a test file -with open('sample.txt', 'w') as f: - f.write("First line\nSecond line\nThird line") +**sample.txt** + +```text +First line +Second line +Third line +``` + +**main.py** -# Reading the entire file at once +```python with open('sample.txt', 'r') as file: content = file.read() - print("File contents:") - print(content) + +print(content) + +``` + +Run `main.py`, and the output is: + +```text +First line +Second line +Third line ``` ### Reading a file line by line -```python -# Reading a file line by line using a loop -with open('sample.txt', 'r') as file: - print("Reading line by line:") - for line in file: - print(f" Line: {line.strip()}") +**sample.txt** + +```text +First line +Second line +Third line ``` -### Reading a specific number of characters +**main.py** ```python -# Reading a specific number of characters with open('sample.txt', 'r') as file: - first_10_chars = file.read(10) - print(f"First 10 characters: {first_10_chars}") + for line in file: + print(f"Line: {line.strip()}") + +``` - # Reading the next 10 characters - next_10_chars = file.read(10) - print(f"Next 10 characters: {next_10_chars}") +Run `main.py`, and the output is: + +```text +Line: First line +Line: Second line +Line: Third line ``` ### Reading all lines into a list +`for line in file` takes lines one at a time and never holds the whole file in memory. That's the default choice, especially for large files. But sometimes you need all the lines at once: to count them, to reach the fifth one, to sort them. That's what `readlines()` is for; it returns an ordinary list: + +**sample.txt** + +```text +First line +Second line +Third line +``` + +**main.py** + ```python -# Reading all lines into a list with open('sample.txt', 'r') as file: lines = file.readlines() - print(f"List of lines: {lines}") + +print(lines) + +``` + +Run `main.py`, and the output is: + +```text +['First line\n', 'Second line\n', 'Third line'] ``` +Notice the `\n` at the end of each element: `readlines()` cuts the file at the line breaks but doesn't throw them away. That's why `line.strip()` showed up in the example above: `print` adds its own line break on top of the one that came from the file, and without `strip()` you'd get blank lines in between. + ## Writing to a File -Writing data to a file can also be done in different ways: +There are several ways to write data to a file: ### Writing a string ```python -# Writing a string to a file with open('output.txt', 'w') as file: file.write("Hello, world!\n") file.write("Python is a great programming language.") @@ -121,7 +176,14 @@ with open('output.txt', 'w') as file: with open('output.txt', 'r') as file: content = file.read() print("File contents after writing:") + +File contents after writing: + print(content) + +Hello, world! +Python is a great programming language. + ``` ### Writing multiple lines @@ -134,70 +196,58 @@ with open('lines.txt', 'w') as file: for line in lines: file.write(line + '\n') -# Alternative way: using writelines() -with open('lines2.txt', 'w') as file: - # Don't forget to add newline characters - file.writelines([line + '\n' for line in lines]) - -# Let's check the second file -with open('lines2.txt', 'r') as file: +# Let's check what was written +with open('lines.txt', 'r') as file: content = file.read() - print("Contents of lines2.txt:") + print("Contents of lines.txt:") + +Contents of lines.txt: + print(content) + +First line +Second line +Third line + + ``` ### Appending data to the end of a file -```python -# Appending data to the end of a file (mode 'a') -with open('output.txt', 'a') as file: - file.write("\nThis line was added later.") +Next to the program sits `output.txt` with two lines β€” the `'a'` mode will append to its end without erasing anything: -# Let's check the result -with open('output.txt', 'r') as file: - content = file.read() - print("File contents after appending:") - print(content) +**output.txt** + +```text +Hello, world! +Python is a great programming language. ``` -## The with Context Manager +**main.py** -You might have noticed that in all the examples above, we used the `with open(...) as file:` construct. -This is a context manager that automatically closes the file after finishing working -with it. +```python +with open('output.txt', 'a') as file: + file.write(" +This line was added later.") -> The `with` context manager is a safe way to work with files, which guarantees that the file will be closed even if errors occur. +with open('output.txt', 'r') as file: + print(file.read()) -Let's compare two approaches: +``` -```python -# Traditional approach (requires explicit file closing) -file = open('example.txt', 'w') -try: - file.write("Example text") -finally: - file.close() # The file needs to be closed explicitly - print("File closed manually") +Run `main.py`, and the output is: -# Approach using with (safer) -with open('example.txt', 'w') as file: - file.write("Example text") -# The file is automatically closed after exiting the with block -print("File automatically closed after the with block") +```text +Hello, world! +Python is a great programming language. +This line was added later. ``` -It is strongly recommended to always use the `with` context manager when working with files in Python! πŸ‘ - ## Exception Handling When Working with Files -Various errors can occur when working with files: - -- The file doesn't exist -- Insufficient permissions to access the file -- The disk is full -- Etc. +Various errors can occur when working with files: the file isn't there, you don't have permission to access it, the disk is full. -It's good practice to handle these errors: +Catching such errors and reacting to them is the job of the `try/except` construct (it has its own lesson later in the course). Here it's enough to see it in action on files: the `try` block attempts the operation, and `except` catches a specific error if it happened. ```python # Handling possible errors when opening a file @@ -206,98 +256,85 @@ try: content = file.read() except FileNotFoundError: print("Error: File not found!") -except PermissionError: - print("Error: Insufficient permissions to access the file!") -except Exception as e: - print(f"An error occurred: {e}") + +Error: File not found! + ``` -## Additional File Operations +## Working with File Paths -Python provides many additional capabilities for working with files: +So far we've written file names plainly: `open('notes.txt')`. Python looks for such a file in the current directory, the one the program was started from. -### Moving the Pointer +If the file sits in a neighbouring folder, you need a path. Gluing one out of strings is risky: on macOS and Linux the parts are separated by `/`, on Windows by `\`. A path written by hand breaks as soon as the code moves to another system. -```python -# Moving the pointer in a file -with open('sample.txt', 'r') as file: - # Let's read the first 5 characters - print(file.read(5)) +So paths are assembled with the `pathlib` module from the standard library. - # Let's move the pointer to the beginning of the file - file.seek(0) - print("After file.seek(0):", file.read(5)) +In `pathlib` a path is an object rather than a string: parts are joined with the `/` operator, and the name, the extension and the existence check live right on the object. Such code reads closer to how a path actually looks. - # Let's move the pointer to the 6th position - file.seek(6) - print("After file.seek(6):", file.read(5)) -``` - -### Getting the Current Position +In the sandbox the current directory is `/home/pyodide`; on your own computer the path will differ. ```python -# Getting the current pointer position -with open('sample.txt', 'r') as file: - print(f"Initial position: {file.tell()}") - file.read(10) - print(f"Position after reading 10 characters: {file.tell()}") - file.readline() - print(f"Position after reading a line: {file.tell()}") -``` - -## Working with File Paths +from pathlib import Path -When working with files, it's important to specify paths correctly. Python provides the `os.path` module and the `pathlib` module to make working with paths easier: +# Current directory +current_path = Path.cwd() +print(f"Current directory: {current_path}") + +Current directory: /home/pyodide + -```python -import os +# Creating a path +data_file = current_path / 'data' / 'info.txt' +print(f"Path to file: {data_file}") + +Path to file: /home/pyodide/data/info.txt + -# Current working directory -current_dir = os.getcwd() -print(f"Current directory: {current_dir}") +# Getting the filename and extension +document_path = Path("path/to/document.pdf") +print(f"Filename: {document_path.stem}, extension: {document_path.suffix}") + +Filename: document, extension: .pdf + +``` -# Joining paths (correctly handling separators) -data_file = os.path.join(current_dir, 'data', 'info.txt') -print(f"Path to file: {data_file}") +Checking existence is the `exists()` method: it answers whether a real file is at that path. Below, `sample.txt` sits next to the program and `missing.txt` does not: -# Checking if a file exists -sample_exists = os.path.exists('sample.txt') -print(f"File sample.txt exists: {sample_exists}") +**sample.txt** -# Getting the filename and extension -filename = "path/to/document.pdf" -basename = os.path.basename(filename) -name, ext = os.path.splitext(basename) -print(f"Filename: {name}, extension: {ext}") +```text +First line +Second line +Third line ``` -A more modern approach using `pathlib`: +**main.py** ```python from pathlib import Path -# Current directory -current_path = Path.cwd() -print(f"Current directory: {current_path}") +print(Path('sample.txt').exists()) +print(Path('missing.txt').exists()) -# Creating a path -data_file = current_path / 'data' / 'info.txt' -print(f"Path to file: {data_file}") +``` -# Checking if a file exists -sample_path = Path('sample.txt') -print(f"File sample.txt exists: {sample_path.exists()}") +Run `main.py`, and the output is: -# Getting the filename and extension -document_path = Path("path/to/document.pdf") -print(f"Filename: {document_path.stem}, extension: {document_path.suffix}") +```text +True +False ``` ## Understanding Check -Let's check how well you've understood the topic of working with files: +**What happens to the contents if you open an existing file in `'w'` mode?** + +1. **Correct answer:** The old contents are wiped and the file starts from scratch β€” The "w" mode clears the file silently, with no warning, and it does so the moment the file is opened β€” before the first write(). To append to the end you need the "a" mode. + +2. The new data is appended, the old data stays β€” That is how the "a" mode behaves. "w" starts the file with a clean slate. -**Which code correctly opens a file for writing and appends a string to the end of the file?** +3. Python refuses to open the file because it already exists β€” An existing file is opened by "w" without complaint. It is the "r" mode that complains the other way round: if the file is missing, it raises FileNotFoundError. +4. Nothing changes until write() is called β€” The file is cleared at the moment it is opened. Even if you close it right away without writing a single line, the previous contents are gone. In future lessons, we'll delve deeper into working with specific file types, such as text files, CSV, JSON, and others. diff --git a/course/module-2/inheritance/article.md b/course/module-2/inheritance/article.md index 2438950..61ffb46 100644 --- a/course/module-2/inheritance/article.md +++ b/course/module-2/inheritance/article.md @@ -1,6 +1,12 @@ +--- +meta: + title: "Inheritance in Python" + description: "How one class extends another: child classes, super(), method overriding, and type checks with isinstance." +--- + # Inheritance in Python -In earlier lessons we built a `Person` class β€” name, age, a `greet()` method. Now we need a `Student` class. A student has a name and age (same as a person), can `greet()` (students also introduce themselves), but also has a school and grades, and greets in their own way β€” mentioning their school. +In earlier lessons we built a `Person` class β€” name, age, a `greet()` method. Now we need a `Student` class. A student has a name and age (same as a person), can `greet()` (students also introduce themselves), but also has a school, and greets in their own way β€” mentioning their school. We could copy all the `Person` code into `Student` and tack on the new bits. But if we ever fix something in `Person`, the copy won't get the update. Duplicated code is a delayed bug. @@ -10,7 +16,7 @@ Inheritance lets us say: "`Student` **is a** `Person`, plus a few extras." Don't To inherit from another class, you put the parent's name in parentheses after the child class name: -```python-executable +```python class Person: def __init__(self, name, age): self.name = name @@ -22,23 +28,30 @@ class Person: class Student(Person): # Student inherits from Person pass # nothing new yet -# Create a student student = Student("Anna", 20) # greet() and attributes come from Person print(student.name) -# Output: Anna + +Anna + print(student.greet()) -# Output: Hi, my name is Anna, I'm 20 years old. + +Hi, my name is Anna, I'm 20 years old. + ``` We haven't written a single line inside `Student`, yet it works β€” because it inherited `__init__` and `greet()` from `Person`. That's inheritance at its simplest. +`class Student(Person)` β€” inherits: `name`, `age`, `greet()` β€” from Person. + +school is declared in Student, while name, age and greet() come from Person + ## Adding new attributes with super() -Now let's give the student a school and grades. We need to extend `__init__`: accept the old parameters (`name`, `age`) and the new ones (`school`, `grades`). To avoid duplicating the `self.name`/`self.age` setup, we'll call the parent's `__init__` via `super()`: +Now let's give the student a school. We need to extend `__init__`: accept the old parameters (`name`, `age`) and the new one (`school`). To avoid duplicating the `self.name`/`self.age` setup, we'll call the parent's `__init__` via `super()`: -```python-executable +```python class Person: def __init__(self, name, age): self.name = name @@ -51,28 +64,26 @@ class Student(Person): def __init__(self, name, age, school): super().__init__(name, age) # let Person set name and age self.school = school - self.grades = [] - - def add_grade(self, grade): - self.grades.append(grade) student = Student("Anna", 20, "MIT") -student.add_grade(5) -student.add_grade(4) print(student.greet()) # inherited method -# Output: Hi, my name is Anna, I'm 20 years old. -print(student.school, student.grades) -# Output: MIT [5, 4] + +Hi, my name is Anna, I'm 20 years old. + +print(student.school) + +MIT + ``` -`super()` is a reference to "the parent of the current class". `super().__init__(name, age)` means "call `Person`'s `__init__` with `name` and `age`". We reuse the parent's logic instead of copying it. +`super()` is a way to reach the parent of the current class. The call in the example reads: "ask `Person` to run its `__init__`, passing `name` and `age`". We reuse the parent's logic instead of copying it. ## Overriding methods A child class can **override** a method from its parent β€” define its own behavior under the same name. If `Student` has its own `greet()`, Python calls that one, not the parent's: -```python-executable +```python class Person: def __init__(self, name, age): self.name = name @@ -94,9 +105,13 @@ person = Person("Ivan", 30) student = Student("Anna", 20, "MIT") print(person.greet()) -# Output: Hi, my name is Ivan. + +Hi, my name is Ivan. + print(student.greet()) -# Output: Hi, my name is Anna. I study at MIT. + +Hi, my name is Anna. I study at MIT. + ``` Inside an overridden method you can still call `super().greet()` to reuse the parent's logic rather than repeat it. @@ -105,7 +120,7 @@ Inside an overridden method you can still call `super().greet()` to reuse the pa Inheritance can chain: `Student` extends `Person`, and `GraduateStudent` extends `Student`. The descendant has access to everything above it in the chain: -```python-executable +```python class Person: def __init__(self, name): self.name = name @@ -127,50 +142,57 @@ grad = GraduateStudent("Anna", "MIT", "Prof. Smith") # Method from Person, attributes from every level print(grad.greet()) -# Output: Hi, I'm Anna. + +Hi, I'm Anna. + print(grad.school, "/", grad.advisor) -# Output: MIT / Prof. Smith + +MIT / Prof. Smith + ``` When Python looks up a method or attribute, it walks the chain: current class first, then the parent, then the parent's parent β€” until it finds something. -## Type checks: isinstance and issubclass +## Type check: isinstance -`isinstance(obj, Class)` checks whether an object is an instance of a class (or any of its subclasses). `issubclass(A, B)` checks whether class `A` descends from `B`: +`isinstance(obj, Class)` checks whether an object is an instance of a class or any of its subclasses: -```python-executable +```python class Person: pass class Student(Person): pass +person = Person() student = Student() -# A Student is a kind of Person print(isinstance(student, Student)) -# Output: True + +True + print(isinstance(student, Person)) -# Output: True - -# Class-to-class relationships -print(issubclass(Student, Person)) -# Output: True -print(issubclass(Person, Student)) -# Output: False + +True + +print(isinstance(person, Student)) + +False + ``` -The key idea: a student **is** a person (`isinstance(student, Person)` is `True`), but a person is not necessarily a student. Inheritance defines an "is-a" relationship that only goes one way. +The key idea: a student **is** a person, so `isinstance(student, Person)` gives `True`. The reverse does not hold: a person is not necessarily a student. -## What about multiple inheritance? +## Understanding check -Python lets a class have multiple parents: `class Duck(Flying, Swimming):`. It works, but the method lookup order in such hierarchies quickly becomes non-obvious (there's a separate algorithm called Method Resolution Order, MRO). In practice, single inheritance covers the vast majority of real code and is easier to read. Multiple inheritance is something to learn later, in an advanced course. +**What happens when a method is called from a child class that doesn't override that method of the parent class?** -## What's next? +1. **Correct answer:** The parent class's method will be called β€” If the method isn't overridden in the child class, Python walks up the parent chain and runs the version it finds. -In the next lesson we'll tackle the second principle of OOP β€” encapsulation: how to hide a class's internals behind an interface and why that makes code more resilient to change. +2. An AttributeError will be raised β€” If the parent has the method, Python will find and run it even though the child class doesn't define it explicitly. ---- +3. Nothing happens, the method just does not run β€” Python walks the inheritance chain, and if it finds the method on the parent, it runs it. -**What happens when a method is called from a child class that doesn't override that method of the parent class?** +4. The method returns None by default β€” The parent's version runs; None is not returned by default. +In the next lesson we'll tackle the second principle of OOP β€” encapsulation: how to hide a class's internals, exposing only what's needed, and why that makes code more resilient to change. diff --git a/course/module-2/lambda-functions/article.md b/course/module-2/lambda-functions/article.md index 3aaa5a2..35cd103 100644 --- a/course/module-2/lambda-functions/article.md +++ b/course/module-2/lambda-functions/article.md @@ -1,210 +1,140 @@ +--- +meta: + title: "Lambda Functions" + description: "Tiny nameless functions: the lambda syntax and its use with sorted, map and filter." +--- + # Lambda Functions in Python -> A lambda function is a small anonymous function that can take any number of arguments but contains only one expression. +A list of numbers `sorted()` handles on its own: with numbers everything is clear β€” smaller, larger. But a list of students raises a question: what should be compared β€” names, grades? Python doesn't know. -Translated from programmer language to human language: +To give it a hint, `sorted()` accepts a small helper function: it takes one student and returns the value to compare by β€” the grade, for example. Defining a full function with `def` for one line like that, naming it, putting it in its own block β€” clunky. -- **Anonymous** β€” it has no name (unlike regular functions defined with `def`) -- **Small** β€” it's limited to one expression (one line of code) -- **Function** β€” it takes arguments and returns a result +For cases like these there are lambda functions: a tiny function right where it's needed, with no name and no `def`. -### Lambda function syntax +> A lambda function is a nameless function of a single expression: it takes arguments and returns the result of that expression. It follows this formula: ```python lambda arguments: expression ``` -Where: - -- `lambda` β€” is the keyword that tells Python we're creating a lambda function -- `arguments` β€” are the input parameters (can be from 0 to several, separated by commas) -- `expression` β€” is a single expression, the result of which will be returned +## A lambda next to a regular function -## Comparison with regular functions - -Let's compare a regular function and a lambda function that do the same thing: +Let's compare a regular function and a lambda that do the same thing: ```python -# Regular function def square(x): return x * x -# Equivalent lambda function square_lambda = lambda x: x * x -# Using both functions print(square(5)) + +25 + print(square_lambda(5)) + +25 + +``` -# Function with multiple arguments -def power(base, exponent): - return base ** exponent - -# Equivalent lambda function -power_lambda = lambda base, exponent: base ** exponent +The notation is shorter: no name, no `return` β€” the result of the single expression is returned. That said, you'll rarely store a lambda in a variable like this: its strength is being passed directly to wherever a small function is needed. Let's start with the task from the introduction. -print(power(2, 3)) -print(power_lambda(2, 3)) -``` +## sorted(): sorting by your own rule -As you can see, lambda functions are more compact but less readable for complex operations. +`sorted()` has a `key` parameter β€” a function that pulls out of each element the value to compare by. The lambda goes right into the call: -## When to use lambda functions? +```python +students = [ + {"name": "Alice", "grade": 85}, + {"name": "Bob", "grade": 92}, + {"name": "Charlie", "grade": 78}, +] -Lambda functions are best suited for cases where: +sorted_by_grade = sorted(students, key=lambda student: student["grade"], reverse=True) +for student in sorted_by_grade: + print(f"{student['name']}: {student['grade']}") + +Bob: 92 +Alice: 85 +Charlie: 78 + +``` -1. The function is simple (one expression) -2. The function is used only once (or a few times in one place) -3. The function is passed as an argument to another function +`key=lambda student: student["grade"]` is that very helper function from the introduction: it takes a student and returns their grade. And `reverse=True` flips the order from highest to lowest. The comparison rule can be anything β€” for example, let's sort numbers by absolute value: -The most common use cases are with higher-order functions such as `map()`, `filter()`, `sorted()`, etc. +```python +numbers = [5, -3, 2, -8, 1, 0, -2] -## Lambda with higher-order functions +print(sorted(numbers, key=lambda x: abs(x))) + +[0, 1, 2, -2, -3, 5, -8] + +``` -### map() β€” applying a function to each element +## map(): apply a function to every element -The `map()` function applies the specified function to each element of an iterable object: +`map()` applies a function to every element of a list, and `list(...)` collects the results into a new list: ```python -# Doubling all numbers in a list numbers = [1, 2, 3, 4, 5] -# With a regular function -def double(x): - return x * 2 - -doubled = list(map(double, numbers)) +doubled = list(map(lambda x: x * 2, numbers)) print(doubled) + +[2, 4, 6, 8, 10] + -# With a lambda function (much more compact!) -doubled_lambda = list(map(lambda x: x * 2, numbers)) -print(doubled_lambda) - -# Converting temperature from Celsius to Fahrenheit -celsius = [0, 10, 20, 30, 40] -fahrenheit = list(map(lambda c: (c * 9/5) + 32, celsius)) +celsius = [0, 10, 20, 30] +fahrenheit = list(map(lambda c: c * 9 / 5 + 32, celsius)) print(fahrenheit) + +[32.0, 50.0, 68.0, 86.0] + ``` -### filter() β€” selecting elements by condition +## filter(): keep elements that pass a condition -The `filter()` function creates an iterator from elements for which the function returns `True`: +`filter()` keeps only the elements for which the function returned `True`: ```python -# Filtering even numbers numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] -# With a lambda function -even_lambda = list(filter(lambda x: x % 2 == 0, numbers)) -print(even_lambda) +even = list(filter(lambda x: x % 2 == 0, numbers)) +print(even) + +[2, 4, 6, 8, 10] + -# Filtering words longer than 3 letters -words = ["hi", "hello", "hey", "howdy", "hi there"] +words = ["hi", "hello", "hey", "howdy"] long_words = list(filter(lambda word: len(word) > 3, words)) print(long_words) + +['hello', 'howdy'] + ``` -### sorted() β€” sorting with a custom key +## When to use a lambda, and when def -The `sorted()` function returns a sorted list, and with the `key` parameter you can specify a function to extract the value for comparison: +A lambda fits when the function is simple β€” a single expression β€” and is needed once, usually as an argument to `sorted()`, `map()` or `filter()`. -```python -# Sorting numbers by absolute value -numbers = [5, -3, 2, -8, 1, 0, -2] +The limitations follow from the notation itself: -sorted_numbers = sorted(numbers, key=lambda x: abs(x)) -print(sorted_numbers) +- the body is exactly one expression: no multiple lines, no assignments inside; +- there's no name, so you can't call it from anywhere else. -# Sorting dictionaries by the value of a specific key -students = [ - {"name": "Alice", "grade": 85}, - {"name": "Bob", "grade": 92}, - {"name": "Charlie", "grade": 78}, - {"name": "Diana", "grade": 95} -] +If the logic doesn't fit into one expression or is needed more than once β€” write a regular function with `def`: it has a name and room for several lines. -# Sorting by grade (descending) -sorted_by_grade = sorted(students, key=lambda student: student["grade"], reverse=True) -for student in sorted_by_grade: - print(f"{student['name']}: {student['grade']}") -``` - -### reduce() β€” sequential application of a function - -The `reduce()` function (from the `functools` module) sequentially applies a function to elements, accumulating the result: - -```python -from functools import reduce - -# Sum of all numbers in a list -numbers = [1, 2, 3, 4, 5] - -total_lambda = reduce(lambda x, y: x + y, numbers) -print(total_lambda) - -# Joining strings -words = ["Hello", "world", "of", "Python"] -sentence = reduce(lambda x, y: x + " " + y, words) -print(sentence) -``` - -## Passing lambda functions to other functions - -Lambda functions are often used as arguments in other functions: - -```python -def apply_operation(x, y, operation): - """Applies an operation to two numbers and returns the result""" - return operation(x, y) - -# Using with different lambda functions -print(apply_operation(5, 3, lambda x, y: x + y)) # Addition -print(apply_operation(5, 3, lambda x, y: x * y)) # Multiplication - -# Formatting data -def format_data(data, formatter): - """Formats data using the specified function""" - return [formatter(item) for item in data] - -names = ["alice", "bob", "charlie"] -print(format_data(names, lambda x: x.title())) # Capitalize first letter -``` +## Understanding check -## Limitations of lambda functions +**Which of the following statements about lambda functions in Python is true?** -Lambda functions have several important limitations: +1. Lambda functions can contain multiple expressions separated by semicolons β€” Lambda functions can contain only a single expression. -1. **Only one expression** β€” you cannot use multiple lines of code -2. **No documentation** β€” you cannot add a docstring -3. **Limited readability** β€” regular functions are better for complex operations -4. **No assignment operator** β€” you cannot use `=` in a lambda +2. Lambda functions are always more efficient than regular functions β€” The efficiency of lambda functions and regular functions is practically the same. -```python -# Example of complex logic where it's better not to use lambda -complex_lambda = lambda x: ( - x ** 2 if x > 0 - else x + 1 if x < 0 - else 42 -) -print(complex_lambda(5)) - -# The same thing but with a regular function - much clearer -def process_number(x): - """Processes a number according to rules: - - Positive -> square - - Negative -> x + 1 - - Zero -> 42 - """ - if x > 0: - return x ** 2 - elif x < 0: - return x + 1 - else: - return 42 - -print(process_number(5)) -``` +3. **Correct answer:** Lambda functions are nameless and can contain only a single expression β€” Lambda functions have no name and are limited to one expression β€” which is why they are written right where they are needed. -## Understanding check +4. You can use the assignment operator (=) inside a lambda function β€” Assignment is not allowed in a lambda: its body is a single expression, not statements. -**Which of the following statements about lambda functions in Python is true?** +In the next lesson β€” error handling: what to do when a program crashes, and how `try/except` turns a failure into a manageable situation. diff --git a/course/module-2/libraries/article.md b/course/module-2/libraries/article.md index 9552495..2b73627 100644 --- a/course/module-2/libraries/article.md +++ b/course/module-2/libraries/article.md @@ -1,72 +1,70 @@ +--- +meta: + title: "Using Libraries in Python" + description: "Introduction to Python libraries: what libraries are, how to import and use them, standard library and third-party modules." +--- + # Using Libraries in Python -Libraries are sets of ready-made code that you can use in your programs. They allow you to avoid "reinventing the wheel" and achieve results faster by using solutions created by other developers. +You need the value of Ο€ or the square root of a number. Deriving the formula and checking it yourself is slow and easy to get wrong, yet someone has already written and debugged that code for you. Ready-made sets of such code are called libraries: you plug the one you need in with a single line and use it. ## What are libraries? > A library (or module) in Python is a file with code containing functions, classes, and variables that you can use in your programs. -Libraries help: - -- Save time by avoiding writing code "from scratch" -- Use proven, optimized solutions -- Extend the capabilities of the Python language -- Create more complex programs with less effort - ## Importing libraries To use a library, you first need to import it into your program. Python offers several ways to import: ### Importing the entire library +We plug in the whole module. Everything inside it stays behind the module's name, and we reach it through a dot: + ```python -# Importing the entire math library import math -# Using functions through the library name radius = 5 circle_area = math.pi * radius ** 2 print(f"The area of a circle with radius {radius} is {circle_area:.2f}") + +The area of a circle with radius 5 is 78.54 + ``` ### Importing specific elements +We take only what we need out of the module. Those names land directly in your program, so there is no `math.` to type in front of them any more: + ```python -# Importing only specific functions from a module -from math import sqrt, pow +from math import sqrt, floor -# Using imported functions directly x = 16 result = sqrt(x) print(f"The square root of {x} is {result}") - -y = pow(2, 3) -print(f"2 to the power of 3 is {y}") + +The square root of 16 is 4.0 + + +y = floor(3.7) +print(f"Floor of 3.7 is {y}") + +Floor of 3.7 is 3 + ``` ### Importing with renaming +The same whole-module import, but under a short name. Handy when the full name is long or already taken by a variable of yours: + ```python -# Importing a module with an alternative name (alias) import math as m angle = 45 -# Using m instead of math sin_value = m.sin(m.radians(angle)) print(f"The sine of {angle} degrees is {sin_value:.4f}") -``` - -### Importing all elements - -```python -# Importing all elements from a module -# Note: this method is generally not recommended -from math import * - -radius = 3 -# Using pi and cos directly, without specifying math -print(f"Ο€ is approximately {pi:.5f}") -print(f"The cosine of 0 is {cos(0)}") + +The sine of 45 degrees is 0.7071 + ``` ## Types of libraries in Python @@ -82,19 +80,21 @@ In Python, there are three main types of libraries: ### Examples of built-in modules ```python -# The random module for generating random numbers import random -# Generating a random number from 1 to 10 random_number = random.randint(1, 10) print(f"Random number: {random_number}") + +Random number: 7 + -# The datetime module for working with dates and times import datetime -# Getting the current date and time current_date = datetime.datetime.now() print(f"Current date and time: {current_date}") + +Current date and time: 2023-07-15 14:30:45.123456 + ``` ## Finding functions in the documentation @@ -103,28 +103,62 @@ When working with libraries, it's important to know how to find information abou ### Using the help() function +`help()` prints everything the module says about itself: what it is for, the list of functions, a description of each. For `math` that help is long, so what you see below is only its beginning, and the ellipsis at the end means it does not stop there. + ```python -# Getting information about a module import math -help(math) # Will only display the beginning of the help to avoid overloading the example +help(math) + +Help on module math: + +NAME + math + +MODULE REFERENCE + https://docs.python.org/3.12/library/math.html + + The following documentation is automatically generated from the Python + source files. It may be incomplete, incorrect or include features that + are considered implementation detail and may vary between Python + implementations. When in doubt, consult the module reference at the + location listed above. + +DESCRIPTION + This module provides access to the mathematical functions + defined by the C standard. + +FUNCTIONS + acos(x, /) + Return the arc cosine (measured in radians) of x. + + The result is between 0 and pi. +... + ``` ### Using the dir() function ```python -# Getting a list of all available attributes and methods import random attributes = dir(random) # Let's display only the first 10 elements for brevity print(attributes[:10]) + +['BPF', 'LOG4', 'NV_MAGICCONST', 'RECIP_BPF', 'Random', 'SG_MAGICCONST', 'SystemRandom', 'TWOPI', '_ONE', '_Sequence'] + ``` ## Understanding Check -Let's check how well you've understood the topic of libraries: - **Which of the following library import methods are correct in Python?** +1. **Correct answer:** import math β€” This is the correct way to import an entire library. After this, functions are used through the module name: math.sqrt(16). + +2. **Correct answer:** from math import sqrt β€” This is the correct way to import a specific function from a module. After this, the function can be used directly: sqrt(16). + +3. import sqrt from math β€” This is incorrect syntax. The correct way is: "from math import sqrt". + +4. import math.sqrt β€” This syntax doesn't work in Python. You cannot import a specific function using dot notation. -In the following lessons, we will look at specific libraries and their applications in more detail. +In the next lesson we'll look at [built-in libraries](https://python-academy.org/en/guide/built-in-libraries) β€” the ones available right after you install Python. diff --git a/course/module-2/modules/article.md b/course/module-2/modules/article.md index 918c467..8071b5d 100644 --- a/course/module-2/modules/article.md +++ b/course/module-2/modules/article.md @@ -1,287 +1,224 @@ +--- +meta: + title: "Modules in Python" + description: "How to create, import, and use your own modules in Python: code organization, namespaces, and best practices." +--- + # Modules in Python -Imagine you're writing a large program that gradually turns into hundreds or even thousands of lines of code. Keeping everything in one file becomes inconvenient β€” the code becomes difficult to maintain and it's hard to find the parts you need. This is where modules come to the rescue! πŸ“¦ +So far every program of yours has fitted into a single file, and that was enough. But one day you write a function that would come in handy in the next task too, and the only way to move it there is to copy it. Copy it into three scripts, find a bug, and you are fixing it in three places. + +So we need a way to keep code in a separate file and pull only what we need out of it. A file like that is called a module. + +## You already use modules -## What is a module? +The line `import math` has come up in the chapters on libraries. Let's look closer at what it actually does. -> A module in Python is simply a file with a `.py` extension, -> containing Python code (functions, classes, variables), -> which can be imported and used in other programs. +```python +import math + +print(math.sqrt(144)) + +12.0 + +print(type(math)) + + + +``` -Modules help: +`import` doesn't paste somebody else's file into yours. It creates a **module object** and puts it in a variable called `math`. From then on `math.sqrt` is an ordinary attribute access, exactly like `"text".upper()`. -- Organize related code into separate files -- Reuse code in different programs -- Avoid name conflicts -- Make code more readable and maintainable +> A module is a file with a `.py` extension (a few built-in ones such as `math` have no file at all, they are baked into the interpreter). Everything defined in it (functions, classes, variables) becomes an attribute of the module object after the import. ## Creating your own module -Creating a module in Python is very simple β€” just write code in a file with a `.py` extension: +Now let's make such a file ourselves. Say we have a small script that works out prices: it adds VAT and applies discounts. The calculation itself goes into `prices.py`, while `main.py` keeps only the printing of results: -Let's create a mymath.py file: +**prices.py** ```python -# mymath.py +"""Price calculations: VAT and discounts.""" -""" -Module with mathematical functions. -""" +VAT = 0.2 -# Constants -PI = 3.14159 -# Functions -def add(a, b): - """Addition of two numbers.""" - return a + b +def with_vat(price): + """Price including VAT.""" + return round(price * (1 + VAT), 2) -def subtract(a, b): - """Subtraction of one number from another.""" - return a - b -def multiply(a, b): - """Multiplication of two numbers.""" - return a * b +def discount(price, percent): + """Price after a percentage discount.""" + return round(price * (1 - percent / 100), 2) -def divide(a, b): - """Division of one number by another.""" - if b == 0: - raise ValueError("Division by zero is not possible") - return a / b ``` -That's it! Now we have a `mymath` module that contains a constant `PI` and four functions. - -## Importing modules - -To use your module in other programs, you need to import it. Python offers several import methods: - -### Importing the entire module +**main.py** ```python -# Assume that the mymath.py file is in the same directory -import mymath +import prices -# Using functions from the module -result = mymath.add(5, 3) -print(f"5 + 3 = {result}") +print(prices.with_vat(100)) # 120.0 +print(prices.discount(1000, 15)) # 850.0 +print(prices.VAT) # 0.2 -# Using a constant from the module -area = mymath.PI * 5**2 -print(f"Area of a circle with radius 5: {area}") ``` -### Importing specific elements +`main.py` says `import prices`, and Python finds `prices.py` in the same folder. The module name comes from the file name: drop the `.py` and you get `prices`. That is all the import needs. -```python -# Importing only specific functions -from mymath import add, multiply +Since the module name is the file name, name your files in snake_case: `user_interface.py`, not `UserInterface.py`. And not `random.py`: a file named after a standard library module shadows the real one. -# Now you can use functions directly, without the mymath prefix -print(f"5 + 3 = {add(5, 3)}") +## A module's code runs exactly once -print(f"5 * 3 = {multiply(5, 3)}") - -# But other functions are not available -# print(subtract(5, 3)) # Will raise an error -``` +On an import Python executes the file top to bottom. In `boot.py` below the `print` sits at the top level, not inside a function, so it fires on import. Let's import that module on two consecutive lines: -### Importing with renaming +**boot.py** ```python -# Importing a module with an alias -import mymath as mm +print("boot.py is running") + +READY = True -print(f"5 - 3 = {mm.subtract(5, 3)}") ``` -### Importing all elements +**main.py** ```python -# Importing all elements from a module -# (this is generally not recommended, as it can lead to name conflicts) -from mymath import * +import boot +import boot -print(f"PI = {PI}") +print("and there were two import lines") -print(f"5 / 2 = {divide(5, 2)}") ``` -## Module structure +```text +boot.py is running +and there were two import lines +``` -A good module should have a clear structure: +Even though there were two `import` lines, the file ran once: Python remembers the modules it has already finished and on a repeated import hands back the same object. Hence the practical takeaway: the top level of a module holds definitions, not actions β€” whatever is written there runs on the very first import, even if none of its functions are ever used. -1. **Documentation** β€” at the beginning of the module (documentation strings in triple quotes) -2. **Imports** β€” first the standard library, then third-party libraries, then your own modules -3. **Constants** β€” global constants -4. **Classes** β€” class definitions -5. **Functions** β€” function definitions -6. **Execution code** β€” code that should be executed when the module is run as a standalone program +## from ... import: the name moves in with you -## Packages +Typing `prices.` before every call gets tiring. The `from ... import` form takes a specific name out of the module and puts it straight into your program. In the example below both import forms sit side by side on purpose, so that the last line can compare the result β€” you wouldn't write it that way in ordinary code. -When your project becomes large, you can organize related modules into packages. +**prices.py** -> A package in Python is a directory containing modules and an `__init__.py` file, which indicates to Python that this directory should be treated as a package. +```python +"""Price calculations: VAT and discounts.""" -Example package structure: +VAT = 0.2 -```text -mypackage/ -β”‚ -β”œβ”€β”€ __init__.py -β”œβ”€β”€ module1.py -β”œβ”€β”€ module2.py -└── subpackage/ - β”œβ”€β”€ __init__.py - └── module3.py -``` -### Creating a package +def with_vat(price): + """Price including VAT.""" + return round(price * (1 + VAT), 2) -1. Create a directory with the package name -2. Add an `__init__.py` file (can be empty) -3. Add your modules (`.py` files) -The `__init__.py` file is executed when the package is imported. It can be empty or contain initialization code: +def discount(price, percent): + """Price after a percentage discount.""" + return round(price * (1 - percent / 100), 2) -```python -# mypackage/__init__.py +``` -""" -My first package. -""" +**main.py** -# Initialization code -print("Package mypackage loaded") +```python +import prices +from prices import with_vat -# Variables available when importing the package -__version__ = '0.1' +print(with_vat(100)) # 120.0 +print(with_vat is prices.with_vat) # True -# You can automatically import modules when importing the package -from . import module1 -from . import module2 ``` -### Importing from a package +The last line is the important one. No copy of the function was made: `with_vat` and `prices.with_vat` are two names for one object. `from ... import` isn't "another way to import", it's an extra step: first the module is imported in full (with all its code), then one of its names is copied into your namespace. -```python -# Assume we have a mypackage as described above +So you have three forms, and you choose between them like this: -# Importing a module from a package -import mypackage.module1 +- **You need many names from the module, or it matters where a name came from** β€” keep the prefix: -# Importing specific elements from a module in a package -from mypackage.module2 import some_function + ```python + import prices -# Importing from a subpackage -import mypackage.subpackage.module3 + print(prices.with_vat(100)) + ``` -# Relative imports (inside package modules) -# from . import module2 # import from the same directory -# from .. import module1 # import from the parent directory -``` +- **You need two or three functions and call them often** β€” take them by name, the code gets shorter: -## Special module variables + ```python + from prices import with_vat, discount -In Python, modules have several special variables: + print(with_vat(100)) + ``` -### name +- **The module name is long or already taken by a variable of yours** β€” bring it in under a short name: -The `__name__` variable allows you to determine whether the module is run as a program or imported: + ```python + import prices as p -```python -# mymodule.py + print(p.with_vat(100)) + ``` -def some_function(): - return "Function executed" +That last form is exactly why people write `import pandas as pd`. -# This code will only run if the module is run directly -if __name__ == "__main__": - print("Module run as a program") - print(some_function()) -else: - print("Module imported") -``` +## Telling a run apart from an import -If you run this module directly: +As we saw, an import executes the whole code of a module. So the code inside `prices.py` runs in both cases: ```bash -$ python mymodule.py -Module run as a program -Function executed +python prices.py # launching the module itself +python main.py # launching the program that imports it ``` -If you import this module: - -```python -import mymodule # Will output: "Module imported" -``` +But sometimes code is only wanted when the file itself is launched. A check along the lines of "do the functions compute the right thing", say: handy for the author, pure noise for whoever imports the module. -### all - -The `__all__` variable defines which names will be imported when using `from module import *`: +The `__name__` variable, which every module has, is what tells the two apart. On an import it holds the module's name, here `"prices"`. In a file that was launched directly it always holds `"__main__"`. So a condition like this is true only on a direct launch: ```python -# mymodule.py - -__all__ = ['public_function', 'PUBLIC_CONSTANT'] - -PUBLIC_CONSTANT = 42 -_PRIVATE_CONSTANT = 43 # Starts with _, considered private - -def public_function(): - return "This is a public function" - -def _private_function(): - return "This is a private function" +if __name__ == "__main__": + print("check:", with_vat(100)) ``` -Now: - -```python -from mymodule import * -# Only available: public_function and PUBLIC_CONSTANT -# _PRIVATE_CONSTANT and _private_function are not imported -``` +In `prices.py` below that check sits at the very bottom, while `main.py` on the next tab simply imports the module: -## Best practices when working with modules +**prices.py** -1. **One module β€” one responsibility**: Each module should be responsible for one specific functionality. +```python +"""Price calculations: VAT and discounts.""" -2. **Clear module names**: Use descriptive but concise names (e.g., `data_processing.py`, `user_interface.py`). +VAT = 0.2 -3. **Documentation**: Each module should begin with documentation describing its purpose. -4. **Imports at the beginning of the file**: All imports should be at the beginning of the module. +def with_vat(price): + """Price including VAT.""" + return round(price * (1 + VAT), 2) -5. **Explicit imports**: Prefer `from module import specific_thing` over `from module import *`. -6. **Private names**: Begin names of "private" functions and variables with an underscore (`_private_function`). +def discount(price, percent): + """Price after a percentage discount.""" + return round(price * (1 - percent / 100), 2) -7. **Module testing**: Use the `if __name__ == "__main__":` block for testing the module. -## Understanding Check +if __name__ == "__main__": + print("check:", with_vat(100)) -Let's check how well you've understood the topic of modules: +``` -**What will be output when executing the following code?** +**main.py** ```python -# file: test_module.py -def hello(): - return "Hello, world!" +import prices -if __name__ == "__main__": - print("Module run directly") -else: - print("Module imported") +print("total:", prices.with_vat(100)) -# file: main.py -import test_module -print(test_module.hello()) ``` +```text +total: 120.0 +``` + +We launched `main.py`, and the output holds only its own line: `__name__` inside `prices.py` equals `"prices"`, the condition is false, the check stayed quiet. Launching the module directly would have printed `check: 120.0`. -Remember, modules and packages help apply the "Don't Repeat Yourself" (DRY) principle and make your programs more professional. Organize your code wisely, and it will be much easier for you to develop it in the future! πŸ’ͺ +This is how one file plays two roles: a set of functions for other code, and a standalone program. That block is where you put everything that must not happen on import: starting the program, parsing command-line arguments, checks like ours. diff --git a/course/module-2/oop/article.md b/course/module-2/oop/article.md index 959e4e4..30fc77e 100644 --- a/course/module-2/oop/article.md +++ b/course/module-2/oop/article.md @@ -1,132 +1,83 @@ -# Python OOP +--- +meta: + title: "OOP in Python" + description: "Introduction to object-oriented programming: what a class and an object are, and which four OOP principles the next chapters cover." +--- -It's time to get acquainted with one of the most important programming paradigms β€” object-oriented programming (OOP). -This is a fundamental approach that will help you create more organized, reusable, and understandable code. 🧩 +# OOP in Python -## What is OOP? +Programs often model things from the real world: a bank account, a user, a car. Every such thing has **data** (balance, name, speed) and **actions** (deposit money, introduce itself, accelerate). -> Object-oriented programming is an approach to software development where a program is built as a collection of objects, each of which is an instance of a specific class, and classes form an inheritance hierarchy. +> Object-oriented programming (OOP) is a way to describe the data and the actions together. The template for such a bundle is called a **class**, and a concrete thing created from the template is an **object**. -In simple terms: OOP allows us to model the real world in code by representing entities as "objects" with characteristics (data) and behavior (functions). +## First example -Imagine you're describing a car. It has: - -- Characteristics: color, make, year of manufacture, current speed -- Behavior: start the engine, accelerate, brake, honk - -In OOP, characteristics become object attributes, and behavior becomes methods. - -## Basic Principles of OOP - -There are four basic principles of OOP: - -1. **Encapsulation** β€” combining data and methods that work with this data into a single object and hiding implementation details -2. **Inheritance** β€” creating new classes based on existing ones while preserving their properties and methods -3. **Polymorphism** β€” the ability to use objects with the same interfaces without information about the type and internal structure of the object -4. **Abstraction** β€” highlighting important characteristics of an object and ignoring non-essential details - -We'll study each of these principles in separate articles, but for now, let's see what OOP looks like in Python. - -## OOP Example in Python - -Here's a simple example of a `Car` class with attributes and methods: +Let's describe a `Car` class and create two cars from it. Don't study the syntax yet: `class`, `__init__` and `self` are all covered step by step in the next chapter. What matters now is the shape: the data and the actions are described in one place. ```python class Car: def __init__(self, make, model): self.make = make self.model = model - self.is_running = False # engine started or not + self.is_running = False def start_engine(self): - """Start the engine""" - if not self.is_running: - self.is_running = True - return f"{self.make} {self.model}: Engine started" - return f"{self.make} {self.model}: Engine was already running" + self.is_running = True + return f"{self.make} {self.model}: engine started" def stop_engine(self): - """Stop the engine""" - if self.is_running: - self.is_running = False - return f"{self.make} {self.model}: Engine stopped" - return f"{self.make} {self.model}: Engine was already stopped" + self.is_running = False + return f"{self.make} {self.model}: engine stopped" -# Create an instance of the Car class my_car = Car("Toyota", "Corolla") - -# Use the object's methods -print(my_car.start_engine()) -print(my_car.stop_engine()) - -# We can create another instance of the Car class another_car = Car("Honda", "Civic") + print(another_car.start_engine()) + +Honda Civic: engine started + +print(my_car.is_running) + +False + +print(another_car.is_running) + +True + ``` -In this example: +We started the engine of the second car only, and only its `is_running` changed. That's the whole point: any number of objects can be created from one class, and each keeps its own data. -- `Car` is a class that describes what a car is -- `my_car` and `another_car` are instances (objects) of the class -- `make`, `model`, `is_running` are attributes -- `start_engine()`, `stop_engine()` are methods +## Class and object -## Benefits of OOP +`class Person` β†’ `object person1`, `object person2`. -OOP offers many benefits: +The terms for all of this: -1. **Modularity** β€” code is divided into logical blocks (classes) that are easier to understand and maintain -2. **Reusability** β€” classes can be used repeatedly in different parts of the program or even in different projects -3. **Scalability** β€” it's easier to add new features without breaking existing functionality -4. **Complexity management** β€” complex systems are easier to model and understand +- `Car` β€” a **class**: the template objects are created from +- `my_car`, `another_car` β€” **objects** (instances) of that class +- `make`, `model`, `is_running` β€” **attributes**: the object's data +- `start_engine()`, `stop_engine()` β€” **methods**: the object's actions -## Practical Example: Banking System +## The four principles of OOP -Let's create a simple model of a bank account: +OOP traditionally rests on four concepts. Each has its own lesson further on (abstraction comes up inside the polymorphism lesson), so for now one sentence about each will do: -```python -class BankAccount: - def __init__(self, owner, balance=0): - self.owner = owner - self.balance = balance - - def deposit(self, amount): - if amount > 0: - self.balance += amount - return f"Deposited ${amount}. New balance: ${self.balance}" - return "Deposit amount must be positive" - - def withdraw(self, amount): - if amount > 0: - if amount <= self.balance: - self.balance -= amount - return f"Withdrew ${amount}. New balance: ${self.balance}" - return "Insufficient funds" - return "Withdrawal amount must be positive" - - def get_balance(self): - return f"{self.owner}'s account balance: ${self.balance}" - -# Create a bank account -account = BankAccount("John Smith", 1000) - -# Use account methods -print(account.get_balance()) -print(account.deposit(500)) -print(account.withdraw(200)) -print(account.withdraw(2000)) # Attempt to withdraw more than the account balance -``` +1. **Encapsulation** β€” packing data and the methods that work with that data into one object. Outside code doesn't reach into the implementation details; it works through the class's interface. +2. **Inheritance** β€” creating a new class on top of an existing one, reusing its attributes and methods. +3. **Polymorphism** β€” working with objects of different classes through a single interface (calling `.area()` the same way on a circle, a square and a triangle). +4. **Abstraction** β€” keeping the essential characteristics of an object and hiding the unimportant details. -This example shows how OOP can be used to model a real object (bank account) with its data (owner, balance) and operations (deposit, withdrawal, balance check). +## Understanding check -## Understanding Check +**How is a class different from an object?** -**What are the main principles of OOP?** +1. **Correct answer:** A class is a template, and an object is a concrete thing created from it β€” Any number of objects can be created from one class, and each gets its own attribute values β€” just like my_car and another_car have their own makes. +2. Class and object are synonyms, there is no difference β€” There is a difference: the Car class in the example is one, while two cars were created from it, each with its own data. -## Conclusion +3. An object is a template, and a class is created from an object β€” The other way round: the template is the class, and objects are created from it by calling Car(...). -You've been introduced to the basics of OOP in Python. -Now you know about the four key principles: encapsulation, inheritance, polymorphism, and abstraction. +4. A class holds the data, and an object holds only the methods β€” The data (attributes) is held by each object separately, while the class describes which attributes and methods every object will have. -In the following articles, we'll explore each of these concepts in more detail. +The next chapter is "Classes": we'll go through all the syntax that flashed by here, step by step. diff --git a/course/module-2/polymorphism/article.md b/course/module-2/polymorphism/article.md index 6d8c6be..ea67e34 100644 --- a/course/module-2/polymorphism/article.md +++ b/course/module-2/polymorphism/article.md @@ -1,3 +1,9 @@ +--- +meta: + title: "Polymorphism" + description: "One call, different behavior: polymorphism through inheritance, abstract base classes, and duck typing." +--- + # Polymorphism Suppose we have a dog, a cat, and a duck, and we need to make each of them `speak()`. Without polymorphism it would look something like this: @@ -27,7 +33,7 @@ for animal in animals: The most common case: a common parent class defines the "contract" (which methods the children must have), and each child implements them its own way. -```python-executable +```python class Animal: def __init__(self, name): self.name = name @@ -50,21 +56,24 @@ class Duck(Animal): animals = [Dog("Rex"), Cat("Fluffy"), Duck("Donald")] for animal in animals: print(animal.speak()) -# Output: -# Rex says: Woof! -# Fluffy says: Meow! -# Donald says: Quack! + +Rex says: Woof! +Fluffy says: Meow! +Donald says: Quack! + ``` Inside the loop we never ask "what kind of object are you?" β€” each animal knows how to speak for itself. Add `class Cow(Animal):` with its own `speak()`, and the loop doesn't change by a single line. +![Illustration: the loop for animal in animals: animal.speak() in the centre, with three arrows reaching down to three boxes β€” Dog (says Woof!), Cat (says Meow!), Duck (says Quack!). Caption above: one call, different behavior](https://python-academy.org/static/guidePage/polymorphism/one-call-many-behaviors-en.webp "One call, different behavior across classes") + ## Abstract classes -Often you want to be sure that **every subclass actually implements the required method**. For example, the base `Shape` class should force every concrete class (Rectangle, Circle, …) to implement `area()`. If someone forgets, it's better to find out immediately, not when the program is already running. +Often you want to be sure that **every subclass actually implements the required method**. For example, the base `Shape` class should force every concrete class (Rectangle, Circle, …) to implement `area()`. If someone forgets, it's better for the error to surface right away β€” when you try to create the object β€” rather than later, when the program reaches the call to that method. -For that Python has **abstract base classes** in the `abc` module: +For that Python has **abstract base classes** in the `abc` module. The required methods are marked with a device you know from the previous lesson β€” the `@abstractmethod` decorator: -```python-executable +```python from abc import ABC, abstractmethod import math @@ -79,7 +88,7 @@ class Shape(ABC): def describe(self): # Uses area() and perimeter() without knowing how they're implemented - return f"Area: {self.area():.2f}, perimeter: {self.perimeter():.2f}" + return f"Area: {round(self.area(), 2)}, perimeter: {round(self.perimeter(), 2)}" class Rectangle(Shape): def __init__(self, width, height): @@ -103,25 +112,28 @@ class Circle(Shape): return 2 * math.pi * self.radius shapes = [Rectangle(5, 3), Circle(4)] -for i, shape in enumerate(shapes, 1): - print(f"Shape {i}: {shape.describe()}") -# Output: -# Shape 1: Area: 15.00, perimeter: 16.00 -# Shape 2: Area: 50.27, perimeter: 25.13 +for shape in shapes: + print(shape.describe()) + +Area: 15, perimeter: 16 +Area: 50.27, perimeter: 25.13 + ``` What `ABC` gave us: -- You can't instantiate `Shape` directly (`Shape()` raises an error). And that's reasonable: "a shape" on its own is meaningless; you need a concrete one. -- If `Circle` had forgotten to define `perimeter()`, Python would refuse to create a `Circle()` at creation time, not later when some missing method gets called. +- Creating a `Shape()` object won't work β€” the attempt fails with an error. And that's reasonable: "a shape" on its own is meaningless; you need a concrete one. +- If `Circle` had forgotten to define `perimeter()`, Python would refuse to create a `Circle()` at creation time, not later when some missing method gets called. Meanwhile `describe()` is defined right in `Shape` and works for any descendant β€” it relies on `area()` and `perimeter()` without knowing their implementations. That's polymorphism through an abstract base class. +It's also the fourth principle of OOP β€” **abstraction**: the code works with "a shape in general" without knowing the details of a concrete one. Outside code only needs to know _what_ a shape can do, not _how_ it does it. + ## Duck typing Python goes one step further: polymorphism **doesn't** require shared inheritance. If an object behaves the right way (has the right methods), it qualifies. People phrase it as: "if it walks like a duck and quacks like a duck, it's a duck". -```python-executable +```python class Duck: def swim(self): return "The duck swims." @@ -141,24 +153,33 @@ def describe(entity): print(entity.sound()) describe(Duck()) -# Output: -# The duck swims. -# Quack quack! + +The duck swims. +Quack quack! + describe(Person()) -# Output: -# The person swims. -# Hello! + +The person swims. +Hello! + ``` The `describe` function doesn't check whether it's looking at a duck or a person. All it cares about is that the object has `swim()` and `sound()` methods. `Duck` and `Person` share no parent class, but polymorphism still works. -This is a very Pythonic approach: instead of "describe the type", "describe the behavior". In practice it often saves you from needless layers of abstraction. +This is a very Pythonic approach: instead of "describe the type", "describe the behavior". In practice it often lets you do without extra parent classes. -## What's next? +## Understanding check -Polymorphism wraps up the four principles of OOP. The key practical effect: code that calls methods through a shared interface doesn't need to be rewritten when new classes appear. Extensibility is built into the architecture. +**Which of the following is an example of polymorphism in Python?** ---- +1. **Correct answer:** Using the same function to process objects of different types β€” This is the main idea of polymorphism: a unified interface for objects of different types. -**Which of the following is an example of polymorphism in Python?** +2. Hiding an internal attribute behind an underscore β€” This is an example of encapsulation, not polymorphism. + +3. Creating a class based on another class β€” This describes inheritance, not polymorphism (although inheritance is often used to implement polymorphism). + +4. Combining data and methods into a single object β€” This describes the general principle of OOP and relates more to encapsulation than to polymorphism. + +Polymorphism wraps up the four principles of OOP. The key practical effect: code that calls methods through a shared interface doesn't need to be rewritten when new classes appear. Extensibility is built into the architecture. +In the next lesson we'll step away from classes and look at lambda functions β€” a way to write a tiny function in a single line. diff --git a/course/module-2/structured-data/article.md b/course/module-2/structured-data/article.md index f697964..92e31e0 100644 --- a/course/module-2/structured-data/article.md +++ b/course/module-2/structured-data/article.md @@ -1,281 +1,228 @@ -# JSON and CSV Formats in Python: Reading, Writing, and Data Analysis +--- +meta: + title: "JSON and CSV Formats in Python: Reading and Writing Data" + description: "Guide to working with structured data in Python: the json and csv modules β€” reading and writing data." +--- -We've already covered the basics of working with files and text files in Python. -Now we're moving on to structured data β€” formats that are used to store and exchange data in a structured way. 🧩 +# JSON and CSV Formats in Python: Reading and Writing Data -We'll focus on two of the most popular formats: +In the previous chapter we parsed a settings file by hand, cutting the lines on the `=` sign. For anything more complex there is no need: ready-made formats exist, and Python reads and writes them with ready-made modules. -- **JSON** (JavaScript Object Notation) β€” a data exchange format widely used in web applications and APIs -- **CSV** (Comma-Separated Values) β€” a simple format for representing tabular data +Let's cover the two most common ones: -These formats are very common and are used in all areas of programming β€” from web development to data analysis. +- **JSON** (JavaScript Object Notation) β€” a data-exchange format used on the web and in APIs +- **CSV** (Comma-Separated Values) β€” a table format; opens in Excel and Google Sheets ## JSON: JavaScript Object Notation -> JSON (JavaScript Object Notation) is a text-based data exchange format, similar to dictionaries and lists in Python. It is easy to read by both humans and machines. +### What JSON Can Hold -### JSON Data Structure +JSON was designed as an exchange format between different languages, so its set of types is deliberately modest, limited to what exists everywhere: -JSON supports the following data types: +| JSON | Python | +| ------- | ----------- | +| object | dict | +| string | str | +| number | int / float | +| boolean | bool | +| null | None | +| array | list | -- Objects (dictionaries): `{"name": "Alice", "age": 30}` -- Arrays (lists): `[1, 2, 3, 4]` -- Strings: `"Hello, world!"` -- Numbers: `42`, `3.14` -- Boolean values: `true`, `false` -- `null` (corresponds to `None` in Python) +The correspondence is almost one to one: only a number has two Python counterparts: a whole one becomes an `int`, a fractional one a `float`. That's what makes JSON so comfortable to use from Python. -### The json Module in Python +### The json Module -Python provides a built-in `json` module for working with this format: +The standard library ships a `json` module for the format, and all the work with it boils down to two actions: turn a Python object into JSON, and parse JSON back into an object. + +The module has four functions. `dump` and `load` work with a file: the first writes a Python object, the second reads it back. `dumps` and `loads` do the same with a string: the `s` at the end stands for **string**. + +`JSON string` ⇄ `json.loads` / `json.dumps` ⇄ `Python object` ⇄ `json.load` / `json.dump` ⇄ `JSON file` + +Let's start with strings: that way the format itself is visible. `dumps` takes two arguments worth passing almost every time. `ensure_ascii=False` keeps non-ASCII characters as they are; without it a city like `ZΓΌrich` ends up in the resulting string as `Z\u00fcrich`: technically correct, humanly unreadable. And `indent=2` adds line breaks and indentation so the result can be read with your eyes. ```python import json -# Simple Python dictionary person = { "name": "Anna", "age": 28, - "city": "Moscow", + "city": "ZΓΌrich", "languages": ["Python", "JavaScript"] } -# Converting a Python dictionary to a JSON string json_string = json.dumps(person, ensure_ascii=False, indent=2) -print("JSON string:") print(json_string) + +{ + "name": "Anna", + "age": 28, + "city": "ZΓΌrich", + "languages": [ + "Python", + "JavaScript" + ] +} + -# Converting a JSON string back to a Python object parsed_data = json.loads(json_string) -print("\nConverted back to Python:") -print(f"Name: {parsed_data['name']}") -print(f"Age: {parsed_data['age']}") -print(f"Programming languages: {', '.join(parsed_data['languages'])}") +print(f"Name: {parsed_data['name']}, languages: {parsed_data['languages']}") + +Name: Anna, languages: ['Python', 'JavaScript'] + ``` ### Writing JSON to a File and Reading from a File -Here's how to save data to a JSON file and then read it: +Usually the data needs more than becoming a string: it needs to land on disk. That's where the pair without the `s` comes in: `dump` writes the object straight into an open file, `load` reads it back, no intermediate string required. ```python import json -# Student data students = [ {"id": 1, "name": "Ivan", "scores": [85, 90, 78]}, {"id": 2, "name": "Maria", "scores": [92, 88, 95]} ] -# Writing to a file with open('students.json', 'w', encoding='utf-8') as file: json.dump(students, file, ensure_ascii=False, indent=2) - print("Data written to students.json file") -# Reading from a file with open('students.json', 'r', encoding='utf-8') as file: loaded_students = json.load(file) - print(f"\nLoaded {len(loaded_students)} students:") - for student in loaded_students: - avg_score = sum(student['scores']) / len(student['scores']) - print(f" {student['name']}: average score {avg_score:.1f}") +for student in loaded_students: + print(f" {student['name']}: scores {student['scores']}") + + Ivan: scores [85, 90, 78] + Maria: scores [92, 88, 95] + ``` -### Main Methods of the json Module - -| Method | Description | -| ---------------------- | ------------------------------------------- | -| `json.dumps(obj)` | Converts a Python object to a JSON string | -| `json.loads(str)` | Converts a JSON string to a Python object | -| `json.dump(obj, file)` | Writes a Python object to a JSON file | -| `json.load(file)` | Reads JSON from a file into a Python object | +## CSV: Comma-Separated Values -The `ensure_ascii=False` parameter allows correct saving of non-ASCII characters and other Unicode symbols, and `indent` makes the output more readable. +### The csv Module -## CSV: Comma-Separated Values +Below are two files: `people.csv` with a small table and `main.py`, which reads it. Start with the `people.csv` tab. That's the core of the format: the first line holds the headers, every following line is a table row, and the columns are separated by commas. -> CSV (Comma-Separated Values) is a simple text format for representing tabular data, where table rows are file lines, and columns are separated by commas (or other delimiters). +Looking at a file like that, it's easy to decide no module is needed: split the line on commas with `split(",")`, and you're done. And that works, but only up to the first comma inside a value: sooner or later one turns up in an address or a product description. The `csv` module knows the rules of the format and takes that trouble off your hands. In `main.py` the file is read by `csv.reader`: -CSV looks something like this: +**people.csv** -```python +```csv Name,Age,City Anna,28,Moscow Ivan,35,Saint Petersburg -``` +Maria,22,Kazan -### The csv Module in Python +``` -Python provides a built-in `csv` module for working with this format: +**main.py** ```python import csv -# Data to write -data = [ - ['Name', 'Age', 'City'], # Headers - ['Anna', '28', 'Moscow'], - ['Ivan', '35', 'Saint Petersburg'], - ['Maria', '22', 'Kazan'] -] - -# Writing to a CSV file -with open('people.csv', 'w', newline='', encoding='utf-8') as file: - writer = csv.writer(file) - writer.writerows(data) - print("Data written to people.csv file") - -# Reading from a CSV file with open('people.csv', 'r', encoding='utf-8') as file: reader = csv.reader(file) + next(reader) # skipping the header line - # Reading headers (first line) - headers = next(reader) - print(f"\nHeaders: {headers}") - - # Reading data - print("\nData:") for row in reader: print(f" {row[0]}, {row[1]} years old, city {row[2]}") + +``` + +Run `main.py`, and the output is: + +```text + Anna, 28 years old, city Moscow + Ivan, 35 years old, city Saint Petersburg + Maria, 22 years old, city Kazan ``` -### Using DictReader and DictWriter +### A Comma Inside a Value -For more convenient work with CSV, you can use `DictReader` and `DictWriter`, -which allow you to work with data as dictionaries: +Back to the problem we promised. What happens when a comma lands inside a value itself, say in a description like "light, thin"? A naive `split(",")` would cut that value in two: one column would turn into two, and the whole row would slide to the right. + +The format solves this with quotes: a value containing a comma is wrapped in quotes, and only commas outside them count as delimiters. You don't place those quotes by hand: writing a table to a file is the job of `csv.writer`, and it adds the quotes itself, while `csv.reader` strips them on the way back. + +The file for writing is opened with `newline=''`: that lets the `csv` module manage the line breaks itself; otherwise blank lines would appear between the table rows on Windows. + +Let's see what the file looks like: ```python import csv -# Writing to CSV using DictWriter -data = [ - {'Name': 'Alex', 'Profession': 'Engineer', 'Salary': 85000}, - {'Name': 'Kate', 'Profession': 'Designer', 'Salary': 75000}, - {'Name': 'Sergey', 'Profession': 'Programmer', 'Salary': 110000} +rows = [ + ['Product', 'Description', 'Price'], + ['Laptop', 'light, thin', '45000'], ] -with open('employees.csv', 'w', newline='', encoding='utf-8') as file: - fieldnames = ['Name', 'Profession', 'Salary'] - writer = csv.DictWriter(file, fieldnames=fieldnames) +with open('quoted.csv', 'w', newline='', encoding='utf-8') as file: + csv.writer(file).writerows(rows) - writer.writeheader() # Writing headers - writer.writerows(data) # Writing data - print("Employee data written to file") +# How it looks in the file +with open('quoted.csv', 'r', encoding='utf-8') as file: + print(file.read(), end="") + +Product,Description,Price +Laptop,"light, thin",45000 + +``` -# Reading from CSV using DictReader -with open('employees.csv', 'r', encoding='utf-8') as file: - reader = csv.DictReader(file) +The quotes appeared in the file and disappeared in Python, and there are still three columns, exactly as before. - print("\nEmployees:") - for row in reader: - print(f" {row['Name']} - {row['Profession']}, salary: {row['Salary']} units") +Quotes are not the format's only subtlety: despite the name, the delimiter is not always a comma. In exports from localised versions of Excel it is usually `;`, because the comma is taken there as the decimal separator. You set the delimiter with the `delimiter` parameter, and it has to be the same when writing and when reading: + +```python +writer = csv.writer(file, delimiter=';') +reader = csv.reader(file, delimiter=';') ``` -### Main Features of Working with CSV +Otherwise the whole line comes back as a single chunk instead of separate columns. -1. **Delimiters**: Although CSV stands for "Comma-Separated Values", in practice other delimiters (semicolon, tab) can be used -2. **Quotes**: If a value contains a delimiter or quotes, it is enclosed in quotes -3. **Escaping**: If there are quotes inside a value, they are escaped +### Addressing Columns by Name -```python -import csv +Up to now rows have arrived as lists, and we reached the value we wanted by number: `row[0]`, `row[1]`, `row[2]`. While there are three columns and the file is your own, that's tolerable. But let someone add a new column in the middle of the export, and every number shifts. The program won't crash: it will quietly start printing the city where the age was expected. A wrong answer with no error is harder to find than a crash. -# Example with a different delimiter -data = [ - ['Product', 'Price', 'In Stock'], - ['Laptop', '45000', 'Yes'], - ['Smartphone', '25000', 'No'] -] +`DictReader` gets rid of the numbers. It looks at the header line and hands back each row as a dictionary whose keys are the column names. Let's read the same `people.csv` with it: -# Writing using semicolon -with open('products.csv', 'w', newline='', encoding='utf-8') as file: - writer = csv.writer(file, delimiter=';') - writer.writerows(data) - print("Data written with delimiter ';'") +**people.csv** -# Reading with the correct delimiter -with open('products.csv', 'r', encoding='utf-8') as file: - reader = csv.reader(file, delimiter=';') - for row in reader: - print(' '.join(row)) -``` +```csv +Name,Age,City +Anna,28,Moscow +Ivan,35,Saint Petersburg +Maria,22,Kazan -## Practical Example: Sales Data Analysis +``` -Let's consider an example where we first save sales data in CSV, then analyze it and save the results in JSON: +**main.py** ```python import csv -import json -# Creating sales data -sales = [ - ['Date', 'Product', 'Category', 'Price', 'Quantity'], - ['2023-01-05', 'HP Laptop', 'Electronics', '45000', '2'], - ['2023-01-10', 'Apple Smartphone', 'Electronics', '85000', '3'], - ['2023-01-15', 'Book "Python"', 'Books', '1200', '5'], - ['2023-02-10', 'Microwave', 'Home Appliances', '7000', '1'] -] +with open('people.csv', 'r', encoding='utf-8') as file: + for row in csv.DictReader(file): + print(f" {row['Name']}, {row['Age']} years old, city {row['City']}") -# Step 1: Save data to CSV -with open('sales.csv', 'w', newline='', encoding='utf-8') as file: - writer = csv.writer(file) - writer.writerows(sales) - print("Sales data saved to CSV") +``` -# Step 2: Read and analyze data -with open('sales.csv', 'r', encoding='utf-8') as file: - reader = csv.reader(file) - headers = next(reader) # Skip headers +Run `main.py`, and the output is: - # Preparing variables for analysis - total_revenue = 0 - sales_by_category = {} +```text + Anna, 28 years old, city Moscow + Ivan, 35 years old, city Saint Petersburg + Maria, 22 years old, city Kazan +``` - # Data analysis - for row in reader: - date, product, category, price, quantity = row - revenue = float(price) * int(quantity) - - # Total revenue - total_revenue += revenue - - # Revenue by category - if category in sales_by_category: - sales_by_category[category] += revenue - else: - sales_by_category[category] = revenue - - # Output analysis results - print(f"\nTotal revenue: {total_revenue} units") - print("\nRevenue by category:") - for category, rev in sales_by_category.items(): - print(f" {category}: {rev} units") - -# Step 3: Save analysis results to JSON -results = { - "total_revenue": total_revenue, - "sales_by_category": sales_by_category -} +The result is the same, but the code holds column names instead of numbers, and a new column in the file shifts nothing. For writing dictionaries there is a matching `DictWriter`: it works the same way, it just asks for the list of column names up front. -with open('sales_analysis.json', 'w', encoding='utf-8') as file: - json.dump(results, file, ensure_ascii=False, indent=2) - print("\nAnalysis results saved to JSON") +## Understanding Check -# Step 4: Check saved JSON -with open('sales_analysis.json', 'r', encoding='utf-8') as file: - saved_results = json.load(file) - print("\nContents of the JSON file with results:") - print(json.dumps(saved_results, ensure_ascii=False, indent=2)) -``` +**How does `json.dumps` differ from `json.dump`?** -In this example we: +1. dumps works with dictionaries, while dump works with lists β€” The data type has nothing to do with it: both functions handle dictionaries and lists alike. -1. Created a CSV file with sales data -2. Read the data and calculated revenue by category -3. Saved the analysis results to a JSON file -4. Read the saved JSON to ensure correctness +2. dumps keeps non-ASCII characters, dump replaces them with codes β€” Non-ASCII characters are the job of a separate argument, \`ensure_ascii=False\`, and both functions accept it. -## Understanding Check +3. **Correct answer:** dumps returns a string, while dump writes straight into an open file β€” The trailing \`s\` stands for string: such a function hands back a string in memory. Without the \`s\` the function takes an open file and writes the data there. -**Which code correctly reads data from a JSON file in Python?** +4. There is no difference, they are two names for one function β€” The functions are different: \`dumps\` returns a string, \`dump\` writes into an open file. diff --git a/course/module-2/text-files/article.md b/course/module-2/text-files/article.md index 9dcfaed..c829fb4 100644 --- a/course/module-2/text-files/article.md +++ b/course/module-2/text-files/article.md @@ -1,234 +1,103 @@ -# Text Files: Encodings, Efficient Processing, and Analysis +--- +meta: + title: "Text Files in Python: Encodings and Parsing Text" + description: "The UTF-8 encoding and what happens when encodings do not match. Parsing a configuration file line by line." +--- -In the previous article, we covered the basics of working with files in Python. -Now, we will delve deeper into working with text files, which are one of the most common types of files in programming. πŸ“ +# Text Files: Encodings and Parsing Text -Text files are used everywhere: for storing configurations, logs, data, program source code, and much more. -The ability to work with them effectively is an essential skill for any programmer. +In the previous article we covered the basics of working with files. Here comes what is specific to text: encodings and parsing the contents line by line. -## Specifics of Working with Text Files +## Encodings -Text files store sequences of characters organized into lines. When working with them in Python, there are several important points to consider: +On disk a file is just a sequence of bytes. To turn them back into letters, you need to know which scheme to use for the conversion: many such schemes have been invented, and different languages historically ended up with different ones. -### Character Encodings - -> An encoding is a way of representing characters as bytes. Different encodings use different schemes to map characters. - -In the modern world, there are many different languages and alphabets. -To allow computers to work with text in different languages, various character encoding systems have been developed: - -- **ASCII** β€” the simplest encoding, containing only Latin letters, numbers, and basic symbols (128 characters in total). -- **UTF-8** β€” the modern standard that supports all languages of the world (including emoji 😊). -- **Windows-1251** (cp1251) β€” an encoding for Cyrillic, popular in Windows. - -In Python, it is recommended to always use UTF-8, especially if your text contains more than just English letters: +The modern standard is **UTF-8**: it covers every alphabet in the world at once, including emoji 😊. The rule for a beginner is simple: write and read in UTF-8, stating the encoding explicitly with the `encoding` parameter. ```python -# Writing text in different encodings -text = "ΠŸΡ€ΠΈΠ²Π΅Ρ‚, ΠΌΠΈΡ€! Hello, world! δ½ ε₯½οΌŒδΈ–η•ŒοΌ" +text = "ΠŸΡ€ΠΈΠ²Π΅Ρ‚, ΠΌΠΈΡ€! Hello, world!" -# Writing in UTF-8 (standard for international texts) with open('text_utf8.txt', 'w', encoding='utf-8') as file: file.write(text) -# Writing in ASCII (only English letters) -try: - with open('text_ascii.txt', 'w', encoding='ascii') as file: - file.write(text) -except UnicodeEncodeError as e: - print(f"ASCII encoding error: {e}") - -# Writing in cp1251 (Cyrillic for Windows) -with open('text_cp1251.txt', 'w', encoding='cp1251') as file: - # Chinese characters will be replaced with '?' - file.write(text) +with open('text_utf8.txt', 'r', encoding='utf-8') as file: + print(file.read()) + +ΠŸΡ€ΠΈΠ²Π΅Ρ‚, ΠΌΠΈΡ€! Hello, world! + ``` -As you can see, trying to write Russian or Chinese text in ASCII encoding causes an error, as ASCII only supports English characters. - -### Reading with the Correct Encoding - -When reading a file, it is important to specify the same encoding with which it was created: +Trouble starts when the encodings don't match: the file was written using one scheme and is read using another. Let's try to read our file as `ascii` β€” an old encoding that only has Latin letters, digits, and punctuation: ```python -# Reading a file with a specified encoding -with open('text_utf8.txt', 'r', encoding='utf-8') as file: - content = file.read() - print(f"Content in UTF-8: {content}") +with open('text_utf8.txt', 'w', encoding='utf-8') as file: + file.write("ΠŸΡ€ΠΈΠ²Π΅Ρ‚, ΠΌΠΈΡ€! Hello, world!") -# Reading with the wrong encoding can lead to errors try: with open('text_utf8.txt', 'r', encoding='ascii') as file: - content = file.read() - print(f"Content with wrong encoding: {content}") + print(file.read()) except UnicodeDecodeError as e: print(f"Decoding error: {e}") + +Decoding error: 'ascii' codec can't decode byte 0xd0 in position 0: ordinal not in range(128) + ``` -### Determining a File's Encoding +The very first byte of a Cyrillic letter doesn't fit into `ascii`, and Python stops with a `UnicodeDecodeError`. The opposite situation looks the same: writing Cyrillic in an encoding that doesn't know it raises a `UnicodeEncodeError`. -Sometimes you receive a text file and don't know what encoding it was saved in. -Python cannot automatically determine the encoding, but there are libraries that can help: +The `try/except` construct is here only to keep the program from breaking off on the error. It has a lesson of its own later in the course, so there is no need to dig into it now. -```python -# The chardet library for detecting encoding -import chardet - -# Create a file in cp1251 -with open('text_cp1251.txt', 'w', encoding='cp1251') as file: - file.write("ΠŸΡ€ΠΈΠ²Π΅Ρ‚, ΠΌΠΈΡ€!") - -# Read the file as bytes and determine the encoding -with open('text_cp1251.txt', 'rb') as file: - raw_data = file.read() - result = chardet.detect(raw_data) - print(f"Detected encoding: {result}") - - # Now we can open the file with the correct encoding - encoding = result['encoding'] - with open('text_cp1251.txt', 'r', encoding=encoding) as text_file: - content = text_file.read() - print(f"Correctly read content: {content}") -``` - -## Efficiently Reading Large Text Files +## Parsing a Configuration File -When working with large text files, it's important to use methods that don't load the entire file into memory. -This is especially critical when you are working with files that are hundreds of megabytes or gigabytes in size. +Program settings are often kept in a text file: every line is a "key = value" pair. Below are two files side by side: `config.ini` with the settings and `main.py`, which builds a dictionary out of them. -### Why You Shouldn't Read the Entire File at Once +**config.ini** -When you use the `read()` method without arguments, Python loads the entire file into memory: +```text +theme = dark +language = en +autosave = True -```python -with open('big_file.txt', 'r') as file: - content = file.read() # The entire file is loaded into memory! ``` -This can cause problems: - -1. **Memory Consumption** β€” if the file is very large (e.g., gigabytes), it can take up all available RAM, leading to a slowdown or even a program crash. -2. **Latency** β€” reading the entire file at once takes time, and your program will "hang" until the reading is complete. -3. **Inefficiency** β€” often, you don't need all the data at once, but rather sequential access to it. - -### Line-by-Line Reading - -A more efficient approach is to read the file line by line using a loop. -Python will load only one line into memory at a time: +**main.py** ```python -# Create a test file with a large number of lines -with open('big_file.txt', 'w') as file: - for i in range(1000): - file.write(f"Line number {i+1}\n") - -# Efficient reading line by line -with open('big_file.txt', 'r') as file: - line_count = 0 - for line in file: # Iterate over lines without loading the whole file into memory - line_count += 1 - if line_count <= 5: # Show only the first 5 lines - print(line.strip()) - print(f"Total lines: {line_count}") -``` - -The advantages of this approach are: - -- Only one line is in memory at a time. -- Processing starts immediately; no need to wait for the entire file to load. -- You can stop reading at any time if you find the data you need. - -### Reading in Chunks - -If you need even more control over the reading process, you can read the file in fixed-size chunks: - -```python -# Reading a file in chunks -with open('big_file.txt', 'r') as file: - block_size = 100 # Block size in bytes - blocks_read = 0 - - while True: - block = file.read(block_size) - if not block: # If the block is empty, the end of the file has been reached - break - - blocks_read += 1 - if blocks_read <= 2: # Show only the first 2 blocks - print(f"Block {blocks_read}: {block[:50]}...") # Print the beginning of the block - - print(f"Total blocks read: {blocks_read}") -``` - -This method allows you to control the amount of memory used for reading the file. -The block size can be adjusted depending on your needs and available memory. - -## Practical Example: Reading and Processing a Configuration File - -Let's look at a simple practical example: reading a configuration file and using its parameters in a program: - -```python -# Create a sample configuration file -config_text = """ -# Database parameters -database_host = localhost -database_port = 5432 -database_name = myapp -database_user = admin -database_password = secret123 - -# Web server parameters -server_port = 8080 -debug_mode = True -log_level = INFO -""" - -with open('config.ini', 'w') as config_file: - config_file.write(config_text) - -# Reading and processing the configuration def read_config(filename): config = {} - with open(filename, 'r') as file: + with open(filename, 'r', encoding='utf-8') as file: for line in file: - # Skip empty lines and comments - line = line.strip() - if not line or line.startswith('#'): - continue - - # Split key and value - if '=' in line: - key, value = line.split('=', 1) - config[key.strip()] = value.strip() + key, value = line.split('=', 1) # cut at the first '=' only + config[key.strip()] = value.strip() return config -# Read the configuration -app_config = read_config('config.ini') -# Use the parameters -print("Application Configuration:") -print(f"Database: {app_config['database_name']} on {app_config['database_host']}:{app_config['database_port']}") -print(f"DB User: {app_config['database_user']}") -print(f"Web Server Port: {app_config['server_port']}") -print(f"Debug Mode: {app_config['debug_mode']}") -``` +settings = read_config('config.ini') -In this example, we: +print(settings) +print(f"Theme: {settings['theme']}, language: {settings['language']}") -1. Created a configuration file with parameters. -2. Wrote a function to read and process the file line by line. -3. Extracted the necessary parameters and used them in the program. +``` + +Run `main.py`, and the output is: -This approach is often used in real applications to store settings in a human-readable format. +```text +{'theme': 'dark', 'language': 'en', 'autosave': 'True'} +Theme: dark, language: en +``` ## Test Your Understanding -Let's check how well you've understood the topic of reading and writing text files: +**A file was written in UTF-8 but opened with `encoding='ascii'`. What happens?** + +1. **Correct answer:** Python stops the program with a UnicodeDecodeError β€” Cyrillic bytes do not fit into ascii, and Python will not guess: the reading breaks off at the first byte that does not fit. + +2. The file is read, but the Cyrillic turns into garbage characters β€” Garbage characters appear when the bytes formally fit some other encoding. Ascii has no Cyrillic bytes at all, so Python reports an error instead of handing you nonsense. -**How do you correctly open a file for reading with UTF-8 encoding?** +3. Python works out the real encoding on its own and reads it correctly β€” Python cannot detect the encoding from the contents. It uses the one you named, or the default one if you named none β€” which is exactly what causes errors on somebody else's file. +4. Only the beginning of the file is read, up to the first Cyrillic letter β€” There is no partial result: the error is raised on the attempt to decode the offending byte, and \`read()\` returns nothing at all. -Now you know the basic principles of working with text files in Python. -In the next article, we will look at working with structured data formats like JSON and CSV. See you there! πŸ‘‹ +In the next article we'll look at structured data formats β€” JSON and CSV. diff --git a/course/module-2/third-party-libraries/article.md b/course/module-2/third-party-libraries/article.md index 9937c63..f06347d 100644 --- a/course/module-2/third-party-libraries/article.md +++ b/course/module-2/third-party-libraries/article.md @@ -1,193 +1,196 @@ -# Third-Party Libraries and pip +--- +meta: + title: "Third-Party Libraries and pip" + description: "Why Python needs third-party libraries, how to install them with pip, why every project gets its own virtual environment, and how to pin dependencies in requirements.txt." +--- + +# Third-party libraries and pip + +Say we have a sales export in front of us: a list of receipts, each with a product, a quantity, and a price. We need the revenue per product. + +Everything required is something we have already covered β€” a dictionary and a loop: + +```python +sales = [ + {"product": "Coffee", "quantity": 3, "price": 150}, + {"product": "Bread", "quantity": 2, "price": 60}, + {"product": "Coffee", "quantity": 1, "price": 150}, + {"product": "Milk", "quantity": 4, "price": 45}, + {"product": "Bread", "quantity": 2, "price": 60}, +] + +revenue = {} +for row in sales: + revenue[row["product"]] = revenue.get(row["product"], 0) + row["quantity"] * row["price"] + +print(revenue) + +{'Coffee': 600, 'Bread': 240, 'Milk': 180} + +``` -Python's built-in libraries are just a basic set of tools, like a screwdriver and a hammer. But to create truly amazing projects, you need specialized tools. And this is where third-party libraries come to the rescue! πŸ› οΈ +It works. The trouble starts with the next request: sort it descending, compute the average receipt, drop anything under a hundred, group by day as well. Every item is one more loop, and an hour later you are looking at three hundred lines nobody wants to touch. -## What are third-party libraries? +Yet the task is an ordinary one; thousands of people solve it every day. So a tool for it was written long ago: the `pandas` library. -> Third-party libraries are Python modules that are not included in the standard library and are developed by independent developers or organizations. +## The same thing in pandas -Third-party libraries help: +```python +import pandas as pd -- Solve specific tasks in different areas -- Save time by using ready-made solutions -- Simplify complex operations that require deep knowledge -- Create more powerful and functional applications +sales = [ + {"product": "Coffee", "quantity": 3, "price": 150}, + {"product": "Bread", "quantity": 2, "price": 60}, + {"product": "Coffee", "quantity": 1, "price": 150}, + {"product": "Milk", "quantity": 4, "price": 45}, + {"product": "Bread", "quantity": 2, "price": 60}, +] -## What is pip? +table = pd.DataFrame(sales) +table["total"] = table["quantity"] * table["price"] -> pip (Package Installer for Python) is a package management system used to install and manage software packages written in Python. +print(table.groupby("product")["total"].sum().to_string()) + +product +Bread 240 +Coffee 600 +Milk 180 + +``` -pip allows you to: +The loop collapsed into a single line that almost reads as English: group by product, take the "total" column, add it up. And the sorting that would have cost you extra code is one more call in the same chain: + +```python +import pandas as pd + +sales = [ + {"product": "Coffee", "quantity": 3, "price": 150}, + {"product": "Bread", "quantity": 2, "price": 60}, + {"product": "Coffee", "quantity": 1, "price": 150}, + {"product": "Milk", "quantity": 4, "price": 45}, + {"product": "Bread", "quantity": 2, "price": 60}, +] + +table = pd.DataFrame(sales) +table["total"] = table["quantity"] * table["price"] + +print(table.groupby("product")["total"].sum().sort_values(ascending=False).to_string()) + +product +Coffee 600 +Bread 240 +Milk 180 + +``` -- Install packages from the Python Package Index (PyPI) and other sources -- Update and remove packages -- Manage dependencies (other packages required for operation) -- Create a list of project dependencies for subsequent installation +This is what third-party libraries are for. Somebody already walked the path from "sum numbers per group" to "filter, recompute and draw a chart", debugged it across thousands of other people's projects, and handed it to you finished. -## Installing and using pip +## Where they come from -Usually pip is already installed along with Python. You can check the presence and version of pip like this: +What arrives together with Python is only the standard library, and `pandas` is not part of it. On a clean machine the line `import pandas` ends in a `ModuleNotFoundError` until the package is installed. -```bash -pip --version -``` +> Third-party libraries are written by independent developers and companies, who publish them to a shared catalogue called [PyPI](https://pypi.org/) (the Python Package Index). That is where you pull them into your project from. -Expected result: +They are downloaded and installed by `pip`, the package manager that arrives together with Python. To check it is there: ```bash -pip 23.1.2 from /usr/local/lib/python3.11/site-packages/pip (python 3.11) +pip --version ``` -### Basic pip commands +## Where to install: the environment comes first -#### Installing a package +By default `pip` puts the package where Python itself lives, and from that moment every program of yours sees it. While there is one program, that is convenient. Once there are two, it turns out the old one needs `django 3.0` and the new one `django 4.2`, and two versions cannot sit in one place. Upgrade for the new one and you break the old one. -```bash -pip install requests -``` +That's why every project gets its own virtual environment: a set of packages that knows nothing about the neighbours. -Expected result: +> A virtual environment (venv) is an isolated copy of Python with its own package folder. Projects stop sharing dependencies and can no longer break each other. -```bash -Collecting requests - Downloading requests-2.31.0-py3-none-any.whl (62 kB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.6/62.6 kB 1.2 MB/s eta 0:00:00 -Installing collected packages: requests -Successfully installed requests-2.31.0 -``` +![Two projects, each with its own venv holding its own package versions: django 3.0 + requests 2.20 in project A, django 4.2 + requests 2.31 in project B](https://python-academy.org/static/guidePage/third-party-libraries/venv-isolation-en.webp "Virtual environments isolate dependencies") -#### Installing a specific version +We create the environment and step into it: -```bash -pip install requests==2.25.1 -``` - -Expected result: +**macOS / Linux** -```bash -Collecting requests==2.25.1 - Downloading requests-2.25.1-py2.py3-none-any.whl (61 kB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.2/61.2 kB 1.8 MB/s eta 0:00:00 -Installing collected packages: requests -Successfully installed requests-2.25.1 +```sh +python -m venv myenv +source myenv/bin/activate ``` -#### List of installed packages +**Windows** -```bash -pip list +```powershell +python -m venv myenv +myenv\Scripts\activate ``` -Expected result: +You can tell you are inside by the start of the prompt, where the environment name shows up: ```bash -Package Version ----------- --------- -certifi 2023.5.7 -charset-normalizer 3.1.0 -idna 3.4 -pip 23.1.2 -requests 2.31.0 -setuptools 67.8.0 -urllib3 2.0.3 +(myenv) $ ``` -#### Removing a package +## Installing a package + +While the environment is active, `pip` puts packages into it rather than into the shared folder: ```bash -pip uninstall requests -y +pip install pandas ``` -Expected result: +If you need a specific version, spell it out after a double equals sign: ```bash -Found existing installation: requests 2.31.0 -Uninstalling requests-2.31.0: - Successfully uninstalled requests-2.31.0 +pip install pandas==2.0.3 ``` -## Popular third-party libraries +To see what is already installed, use `pip list`. To remove something, `pip uninstall pandas`. -Python has a huge number of third-party libraries for a wide variety of tasks. Here are some of the most popular and useful: +## So the same thing builds for a colleague -| Library | Description | Main application | Link | -| ------------------ | ------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------ | -| **Requests** | Convenient handling of HTTP requests | Interaction with web resources, APIs | [Documentation](https://requests.readthedocs.io/) | -| **Pandas** | Powerful tool for data analysis | Processing and analyzing tabular data | [Documentation](https://pandas.pydata.org/) | -| **NumPy** | Working with arrays and mathematical calculations | Scientific computing, array operations | [Documentation](https://numpy.org/) | -| **Matplotlib** | Data visualization | Creating plots and charts | [Documentation](https://matplotlib.org/) | -| **Seaborn** | Statistical data visualization | Beautiful statistical graphics | [Documentation](https://seaborn.pydata.org/) | -| **scikit-learn** | Machine learning | Building and training machine learning models | [Documentation](https://scikit-learn.org/) | -| **TensorFlow** | Deep learning and neural networks | Complex deep learning models | [Documentation](https://www.tensorflow.org/) | -| **PyTorch** | Deep learning with dynamic computational graphs | Research in deep learning | [Documentation](https://pytorch.org/) | -| **Flask** | Micro-framework for web development | Creating web applications and APIs | [Documentation](https://flask.palletsprojects.com/) | -| **Django** | Full-featured web framework | Large web projects | [Documentation](https://www.djangoproject.com/) | -| **Beautiful Soup** | Parsing HTML and XML | Extracting data from web pages | [Documentation](https://www.crummy.com/software/BeautifulSoup/) | -| **Pillow** | Image processing | Editing and analyzing images | [Documentation](https://pillow.readthedocs.io/) | -| **SQLAlchemy** | ORM for working with databases | Interaction with SQL databases | [Documentation](https://www.sqlalchemy.org/) | -| **Pygame** | Creating games and multimedia applications | 2D game development | [Documentation](https://www.pygame.org/) | -| **PyQt** | Creating desktop applications | Graphical user interfaces (GUI) | [Documentation](https://www.riverbankcomputing.com/software/pyqt/) | - -The choice of library depends on the specific task you want to solve. The Python community is very active, and ready-made solutions already exist for most practical tasks, which can be installed via pip. - -Example of installing a popular library: +The environment stays on your machine, while only the code travels to the repository. So that your colleague and the server end up with exactly the same versions, the dependency list is saved to a file: ```bash -# Installing pandas for data analysis -pip install pandas - -# Installing Flask for web development -pip install flask +pip freeze > requirements.txt ``` -## Virtual environments - -When working with different projects, you often need to use different versions of libraries. For this purpose, Python has virtual environments. +Inside it, one package and its exact version per line: -> A virtual environment is an isolated Python environment where you can install packages without affecting other projects or the system Python. +```text +numpy==1.25.2 +pandas==2.0.3 +python-dateutil==2.8.2 +``` -### Creating a virtual environment +The file goes into the repository next to the code, and from then on anyone who clones the project reproduces the environment with one command: ```bash -# Creating a virtual environment -python -m venv myenv +pip install -r requirements.txt +``` -# Activating the virtual environment -# On Windows: -myenv\Scripts\activate +The versions are pinned, so what they get is exactly what you have, instead of "well, it works on my machine". -# On macOS/Linux: -source myenv/bin/activate +## Where to go next -# After activation, the environment name will appear at the beginning of the command prompt -(myenv) $ -``` - -### Installing packages in a virtual environment +PyPI holds hundreds of thousands of packages, but a handful is enough to start with: -```bash -# Installing packages in the activated virtual environment -pip install pandas matplotlib -``` +| When you'll need it | Library | +| -------------------------------- | ------------------ | +| Tables, reports, analytics | `pandas` | +| HTTP calls to someone's API | `requests` | +| Pulling data out of an HTML page | `beautifulsoup4` | +| Charts and diagrams | `matplotlib` | +| Your own web service or API | `flask`, `fastapi` | -### Saving and installing dependencies +The rest you look up on PyPI at the moment you hit the task. There is no need to memorise the list in advance πŸŽ“. -```bash -# Saving the list of installed packages -pip freeze > requirements.txt +## Understanding Check -# The contents of the requirements.txt file will look something like this: -# matplotlib==3.7.2 -# numpy==1.25.2 -# pandas==2.0.3 -# ... +**Why does every project get its own virtual environment?** -# Installing packages from the requirements.txt file -pip install -r requirements.txt -``` +1. **Correct answer:** So projects don't share package versions and break each other β€” Correct. Different projects need different versions of the same library, and one shared folder can only hold one of them. A separate environment removes the conflict. -## Understanding Check +2. So the code in the project runs faster β€” An environment has no effect on execution speed: it is about where packages live, not about how the code runs. -Let's check how well you've learned about third-party libraries and pip: +3. So you don't have to install pip separately for each project β€” pip arrives together with Python, there is no need to install it separately. The environment solves a different problem β€” conflicting versions. -**Which command will correctly install pandas version 1.5.0?** +4. So Python doesn't have to be installed on the computer at all β€” The Python installed on your computer is still needed: it is what creates the virtual environment via python -m venv. diff --git a/course/module-2/type-annotations/article.md b/course/module-2/type-annotations/article.md index c599330..e8d216d 100644 --- a/course/module-2/type-annotations/article.md +++ b/course/module-2/type-annotations/article.md @@ -1,139 +1,115 @@ -# Type Annotations in Python +--- +meta: + title: "Type Annotations in Python" + description: "How to hint argument and result types: annotation syntax, collections, the X | None notation and type aliases." +--- -In large projects, it can be difficult to tell at a glance what data a function accepts or exactly what a variable holds. To make code more predictable and safe, Python introduced **type annotations**. +# Type Annotations in Python -Compare two versions of the same function: +In large projects it can be hard to tell at a glance what data a function accepts. Compare two versions of the same stub: ```python -# Before: what is user? An object? Database record? What does it return? +# What is user? An object? A string with a name? What does the function return? def get_discount(user): pass -# After: it is instantly clear we pass an ID (integer) and receive a discount (float) +# Immediately clear: we pass an ID (integer), we get a discount (float) def get_discount(user_id: int) -> float: pass ``` -Annotations help developers and IDEs (Integrated Development Environments) clearly understand what data types are expected in different parts of the program, significantly speeding up coding and debugging. +The `: int` and `-> float` marks are **type annotations**: hints about what types of values the function takes and returns. -## What are Type Annotations? +> **Type annotations** are a special syntax for explicitly stating the expected data types of variables, function arguments and return values. -> **Type Annotations (Type Hints)** are a special syntax in Python that allows you to explicitly specify the expected data types for variables, function arguments, and return values. +## Hints, not restrictions -It is crucial to understand: **Python remains a dynamically typed language**. Type annotations are merely _hints_. Python itself won't stop the execution of your program if you pass a string instead of a number at runtime. However, they are indispensable for: +Python remains a dynamically typed language: at runtime an annotation checks nothing. Let's make sure: -- Improving code autocompletion in your IDE (like PyCharm or VS Code) -- Static code analysis (using tools like `mypy`) -- Self-documenting code (making the code much easier to read and understand) +```python +def double(x: int) -> int: + return x * 2 -## Basic Syntax +print(double(5)) + +10 + +print(double("ha")) # the annotation didn't stop the string + +haha + +``` -### Variable Annotation +The program didn't crash: to Python an annotation is just a note. The value comes from the tools around your code β€” the editor and the IDE. We'll see exactly how at the end of the lesson. -To annotate a variable, place a colon `:` after the variable name, followed by its type: +## Basic syntax -```python -# Annotating basic types +For a variable, the type goes after the name, separated by a colon: + +```python-interactive name: str = "Alex" age: int = 28 height: float = 1.82 is_developer: bool = True ``` -### Function Annotation - -For functions, we can specify types for the passed arguments and the type of the return value (using the arrow `->`): +In functions you annotate the arguments and β€” with the `->` arrow β€” the return value: ```python def greet(name: str, age: int) -> str: - return f"Hello, {name}! You are {age} years old." + return f"Hi, {name}! You are {age} years old." -message = greet("John", 25) +message = greet("Ivan", 25) print(message) + +Hi, Ivan! You are 25 years old. + ``` -In this example: +If a function returns nothing, write `-> None`. -- `name: str` β€” a string is expected -- `age: int` β€” an integer is expected -- `-> str` β€” the function promises to return a string +## Annotating collections -_(Note: if a function does not return anything, we use `None` as the return type, e.g., `-> None`)._ +For collections you also state the type of their contents: -## Typing Collections (Lists, Dictionaries) - -You can use standard collections to annotate their contents: - -```python -# A list of integers +```python-interactive numbers: list[int] = [1, 2, 3, 4, 5] -# A dictionary where keys are strings and values are integers +# Keys are strings, values are numbers user_ages: dict[str, int] = { - "John": 25, + "Ivan": 25, "Anna": 22 } -# A tuple with a strict structure: (string, integer, float) +# A tuple with a fixed structure: (string, integer, float) user_info: tuple[str, int, float] = ("Alex", 30, 75.5) - -# A set of strings -unique_names: set[str] = {"John", "Anna", "Peter"} ``` -## The typing Module +## A value or None -For more complex scenarios, we use the built-in `typing` module: - -### Optional - -If a variable can contain a value of a specific type **or** `None`, use `Optional`: - -```python -from typing import Optional +A common case: a function returns a value β€” or `None` when nothing was found. This is written with a vertical bar: -def get_user_email(user_id: int) -> Optional[str]: +```python-interactive +def get_user_email(user_id: int) -> str | None: if user_id == 1: return "admin@example.com" - return None # Return None if the user is not found + return None ``` -### Union +The same bar joins any types: `int | float` means "an integer or a float": -When a variable can take one of several possible types: - -```python -from typing import Union - -# The function can accept either an integer or a float -def process_price(price: Union[int, float]) -> float: - return float(price) * 1.2 # Add 20% tax +```python-interactive +def process_price(price: int | float) -> float: + return float(price) * 1.2 ``` -### Callable (Functions as Arguments) +> In older code you'll see `Optional[str]` instead of `str | None`, and `Union[int, float]` instead of `int | float`, from the `typing` module. They mean the same thing: the `|` notation appeared in Python 3.10 and is now preferred. -If you pass a function as an argument to another function, you can type it as well. +## Type aliases -`Callable` takes two arguments: a list of input parameter types and the return type: - -```python -from typing import Callable - -def apply_twice(value: int, func: Callable[[int], int]) -> int: - return func(func(value)) - -def double(x: int) -> int: - return x * 2 +To avoid repeating long types over and over, give them names: -result = apply_twice(3, double) # double(double(3)) = 12 -``` - -## Creating Custom Types (Type Aliases) - -To avoid writing long, complex types multiple times, you can create **type aliases**: - -```python -# Creating an alias +```python-interactive Coordinates = tuple[float, float] UserDict = dict[str, str | int] @@ -144,14 +120,20 @@ def process_user(user: UserDict) -> None: pass ``` -## Why Do We Need All This? +## Why bother + +1. **Fewer bugs**: the editor underlines a mismatch before you even run the program, if you pass a string where an `int` is expected. +2. **Precise autocompletion**: the IDE knows the value's type β€” so it knows its methods. +3. **Easier reading**: the header `def get_user(user_id: int) -> dict[str, str]:` tells the whole story without reading the body. + +## Understanding check + +**How do you describe a function that takes an integer and a string-or-None, and returns a list of numbers?** -1. **Fewer Bugs**: Your code editor will highlight an error in red _even before running the program_ if you try to pass a string `""` where an `int` is expected. -2. **Perfect Autocompletion**: The IDE will know exactly what methods are available on the object because you specified its type. -3. **Easier to Read Code**: Reading the signature `def get_user(user_id: int) -> dict[str, str]:`, you instantly understand that the function takes a numeric ID and returns a dictionary. You don't have to read through the entire body of the function to figure that out. +1. def process(id: int, name: str) -> list\[int]: β€” Here name is a required string, while the task says it may also be None. -## Checking Your Understanding +2. def process(id: int, name: str | None) -> list: β€” The returned list has no content type β€” it should be list\[int]. -Let's check your knowledge of type annotations: +3. **Correct answer:** def process(id: int, name: str | None) -> list\[int]: β€” str | None allows a string or None, and list\[int] specifies what the list contains. -**How do you properly annotate a function that takes an integer and an optional string (which can be None), and returns a list of integers?** +4. def process(id: Integer, name: str | None) -> list\[int]: β€” Python's built-in type is called int, not Integer. diff --git a/course/module-3/asyncio-advanced/article.md b/course/module-3/asyncio-advanced/article.md index c7ff65a..da32e18 100644 --- a/course/module-3/asyncio-advanced/article.md +++ b/course/module-3/asyncio-advanced/article.md @@ -1,6 +1,14 @@ +--- +meta: + title: "Advanced asyncio: queues, synchronization, executor" + description: "asyncio.Queue for sharing data between coroutines, asyncio.Lock for synchronization, and most importantly run_in_executor for blocking code without stalling the event loop." +--- + # Advanced asyncio in Python -The previous article covered the basics of asyncio: `async def`, `await`, `gather`, Tasks. Here are three tools you'll reach for in real applications: queues between coroutines, synchronization, and (most importantly) running blocking code without stalling the event loop. +An async service was holding thousands of connections and then froze all at once, for everyone. The cause was one line: someone called a synchronous `requests.get()`, and for a second it blocked the single event-loop thread, and with it every other coroutine. + +The asyncio basics from the previous article (`async def`, `await`, `gather`, Tasks) aren't enough here. For real applications you need three more tools: queues between coroutines, synchronization, and, above all, running blocking code without stalling the event loop. ## asyncio.Queue: passing data between coroutines @@ -63,10 +71,12 @@ Besides `Lock` there's `asyncio.Event`, `asyncio.Semaphore`, `asyncio.Condition` ## Blocking code in asyncio: run_in_executor -The cardinal rule of asyncio: **never call blocking functions directly**. `time.sleep(2)`, `requests.get()`, heavy math β€” anything blocking stalls the event loop and freezes every other coroutine. +Back to the outage from the start of the chapter. The rule broken there: **never call blocking functions directly in the event loop**. `time.sleep(2)`, `requests.get()`, heavy math β€” anything blocking stalls the whole loop. But sometimes there's no avoiding it: an old synchronous library, a CPU-bound calculation. For that there's `loop.run_in_executor()`: run a blocking function in a **separate thread** (or process) while the event loop keeps going. +![Illustration: event loop with await blocking_task() on the left; arrow labeled run_in_executor leads to a Thread Pool Executor on the right where time.sleep(2) runs; below, other coroutines keep running; arrow returns the result back to the event loop](https://python-academy.org/static/guidePage/asyncio-advanced/run-in-executor-en.webp "run_in_executor offloads a blocking function into a thread pool so the event loop is not blocked") + ```python import asyncio import time @@ -118,40 +128,39 @@ You'll rarely write these yourself; they're tools libraries (`aiohttp`, `asyncpg ## Comparing the three approaches -| | threading | multiprocessing | asyncio | -| --- | --- | --- | --- | -| CPU parallelism | no (GIL) | yes | no (1 thread) | -| I/O-bound | good | good but expensive | great | -| Overhead | low | high | minimal | -| Memory | shared | isolated | shared (1 thread) | -| Sharing data | variables + Lock / Queue | Queue, Pipe, Manager | variables / asyncio.Queue | -| Thousands of tasks | poor | very poor | excellent | +| | threading | multiprocessing | asyncio | +| ------------------ | ------------------------ | -------------------- | ------------------------- | +| CPU parallelism | no (GIL) | yes | no (1 thread) | +| I/O-bound | good | good but expensive | great | +| Overhead | low | high | minimal | +| Memory | shared | isolated | shared (1 thread) | +| Sharing data | variables + Lock / Queue | Queue, Pipe, Manager | variables / asyncio.Queue | +| Thousands of tasks | poor | very poor | excellent | **Rule of thumb:** -- Thousands of network connections, new projects β†’ **asyncio** -- I/O in legacy synchronous code without async libraries β†’ **threading** or `ThreadPoolExecutor` -- Heavy computation β†’ **multiprocessing** or `ProcessPoolExecutor` -- Real apps often mix all three: asyncio as the main layer + `run_in_executor` with a thread/process pool for blocking pieces. +- Thousands of network connections, new projects β†’ **asyncio** +- I/O in legacy synchronous code without async libraries β†’ **threading** or `ThreadPoolExecutor` +- Heavy computation β†’ **multiprocessing** or `ProcessPoolExecutor` +- Real apps often mix all three: asyncio as the main layer + `run_in_executor` with a thread/process pool for blocking pieces. ## A few pitfalls -- **CPU-bound in asyncio** kills the event loop. Use `run_in_executor` with a `ProcessPoolExecutor` for heavy math inside async code. -- **Forgotten `await`**: `asyncio.sleep(1)` without `await` does nothing (it creates a coroutine that gets discarded). Modern IDEs highlight this. -- **Mixing sync/async**: calling `requests.get()` (synchronous) inside asyncio stalls everything. Use `aiohttp` or `httpx` for async HTTP. -- **`if __name__ == "__main__":`** on Windows and macOS is required for `multiprocessing`; otherwise child processes recursively spawn themselves. +- **CPU-bound in asyncio** kills the event loop. Use `run_in_executor` with a `ProcessPoolExecutor` for heavy math inside async code. +- **Forgotten `await`**: `asyncio.sleep(1)` without `await` does nothing (it creates a coroutine that gets discarded). Modern IDEs highlight this. +- **Mixing sync/async**: calling `requests.get()` (synchronous) inside asyncio stalls everything. Use `aiohttp` or `httpx` for async HTTP. +- **`if __name__ == "__main__":`** on Windows and macOS is required for `multiprocessing`; otherwise child processes recursively spawn themselves. -## What's next? +## Understanding check -That wraps up the concurrency module. The key rule: pick the tool by task type. +**Which asyncio tool runs blocking code without stalling the event loop?** -- Thousands of network connections β†’ **asyncio** -- I/O in legacy code β†’ **threading** -- Math and data crunching β†’ **multiprocessing** +1. asyncio.sleep() β€” asyncio.sleep() is a non-blocking pause; it's already async itself. -In modern apps the main program tends to be asyncio, with CPU-heavy pieces offloaded into a process pool through `run_in_executor`. +2. asyncio.gather() β€” gather() runs several coroutines concurrently. It doesn't help with blocking SYNCHRONOUS functions β€” they would still freeze the event loop. ---- +3. **Correct answer:** loop.run_in_executor() β€” Correct. run_in_executor pushes a blocking function into a separate thread (or process), and the event loop keeps running other coroutines. -**Which asyncio tool runs blocking code without stalling the event loop?** +4. asyncio.create_task() β€” create_task() schedules a coroutine on the event loop. It doesn't help with SYNCHRONOUS blocking functions β€” they would still freeze the loop. +That wraps up the concurrency module. The map from the intro article and the matrix above already answer "which tool"; in practice the main program most often lives on asyncio, handing CPU-heavy pieces to a process pool through `run_in_executor`. diff --git a/course/module-3/asyncio-basics/article.md b/course/module-3/asyncio-basics/article.md index b4c13e9..c876f61 100644 --- a/course/module-3/asyncio-basics/article.md +++ b/course/module-3/asyncio-basics/article.md @@ -1,6 +1,12 @@ +--- +meta: + title: "asyncio basics: async/await, coroutines, the event loop" + description: "Basic asynchronous programming in Python: async and await keywords, coroutines, the event loop, tasks, and asyncio.gather." +--- + # asyncio basics in Python -The previous article covered threads and processes. Both share the same model: the OS switches between "workers". **asyncio** takes a different path: a single thread with cooperative multitasking. The program itself marks the places where a task can be "deferred"; those places are `await`. +Three downloads from the intro article took a second and a half β€” and for almost all of it the CPU just waited for the server to answer. Threads and processes solved that by handing the switching to the operating system. **asyncio** takes a different route: a single thread with cooperative multitasking. While one request waits on the server, that same thread picks up the next, and the program itself marks the points where a task can be "parked" with the keyword `await`. For I/O-bound workloads, asyncio gives the best ratio of throughput to resources: thousands of concurrent connections on a single thread with no OS-thread overhead. @@ -8,14 +14,16 @@ For I/O-bound workloads, asyncio gives the best ratio of throughput to resources The heart of asyncio is the **event loop**. It keeps a list of tasks, runs one of them, and when a task hits `await something_slow()`, the task "yields control" and the loop switches to the next ready task. When `something_slow()` finishes, the original task becomes ready again. +![Illustration: in the centre an Event Loop, around it three tasks (Task 1 β€” running, Task 2 β€” awaiting I/O, Task 3 β€” ready); arrows between the tasks and event loop show how control is handed back and forth](https://python-academy.org/static/guidePage/asyncio-basics/event-loop-en.webp "The event loop hands control between tasks on every await") + Important: switching happens **only at `await`**. There are no interruptions in the middle of a computation. It's "cooperative" multitasking: tasks agree on when to yield. Consequence: if a task doesn't hit `await` (for example, it does heavy CPU work), the entire event loop is stuck. ## async and await Python 3.5 introduced two keywords: -- `async def` defines a **coroutine** (an asynchronous function) -- `await` inside a coroutine: "wait for this operation, and let the event loop run other things while you wait" +- `async def` defines a **coroutine** (an asynchronous function) +- `await` inside a coroutine: "wait for this operation, and let the event loop run other things while you wait" ```python import asyncio @@ -120,11 +128,16 @@ asyncio.run(main()) 2. **Want them to run concurrently? `asyncio.gather()` or `asyncio.create_task()`**. Plain stacked `await` = sequential. 3. **CPU-bound in asyncio stalls everything**. Long computation? Move it to `run_in_executor` (next article) or `multiprocessing`. -## What's next? +## Understanding check -The next article covers asyncio's advanced techniques: queues, coordination between coroutines, and (the most important one) how to run blocking code without killing the event loop. +**What is the core idea of asyncio's concurrency model?** ---- +1. True parallelism on multiple CPU cores β€” That describes multiprocessing. asyncio runs on a single thread and doesn't help CPU-bound work. -**What is the core idea of asyncio's concurrency model?** +2. **Correct answer:** Cooperative multitasking: tasks explicitly hand control back to the event loop at await β€” Correct. Coroutines yield control at await, letting other tasks run while one waits for I/O. This is the fundamental principle of asyncio. + +3. Automatic parallelization of any function without async/await β€” Not automatic. A function becomes a coroutine only via async def, and switching only happens at await. +4. Creating a system thread per task β€” The opposite β€” asyncio runs in a single thread. That's exactly its win for I/O: no thread-creation overhead. + +The next article covers asyncio's advanced techniques: queues, coordination between coroutines, and (the most important one) how to run blocking code without killing the event loop. diff --git a/course/module-3/coverage-and-ci/article.md b/course/module-3/coverage-and-ci/article.md index 3b5f5df..0291cbe 100644 --- a/course/module-3/coverage-and-ci/article.md +++ b/course/module-3/coverage-and-ci/article.md @@ -1,6 +1,12 @@ +--- +meta: + title: "Code coverage and CI: automating tests" + description: "Measure test coverage with pytest-cov and run tests automatically with GitHub Actions on every push. The basic production setup." +--- + # Code coverage and CI -Tests that sit in a repo and never run don't guarantee anything. To make tests actually work, you need two things: +Forty tests in the repo, all green. Except the last time anyone ran them was eleven days ago β€” and a dozen changes have landed on `main` since. They're green only on paper: nobody knows whether they still pass. For tests to actually guarantee anything, you need two things: 1. **Coverage**: know what parts of the code your tests actually exercise, and what they don't. 2. **CI** (Continuous Integration): so tests run automatically on every push, not "sometime" by hand. @@ -33,9 +39,9 @@ app/orders.py 18 0 100% TOTAL 43 5 88% ``` -- `Stmts`: how many statements are in the file. -- `Miss`: how many didn't execute during the tests. -- `Cover`: percentage of coverage. +- `Stmts`: how many statements are in the file. +- `Miss`: how many didn't execute during the tests. +- `Cover`: percentage of coverage. For a detailed report (with line-by-line highlighting in a browser) use HTML: @@ -53,6 +59,8 @@ Chasing 100% usually isn't worth it. A realistic target is **80-90%** for busine **CI** is automatic test (or any other check) runs on repository changes. On GitHub it's **GitHub Actions**, on GitLab it's the built-in GitLab CI, there's also CircleCI, Jenkins, and others. The principle is the same everywhere: on every `git push`, a configured pipeline runs. +![Illustration: git push β†’ GitHub Actions runs automatically β†’ pytest + coverage β†’ branches to passed (green) or failed (red)](https://python-academy.org/static/guidePage/coverage-and-ci/ci-pipeline-en.webp "CI runs tests on every push") + ## A minimal CI on GitHub Actions Put `.github/workflows/tests.yml` at the root of the repo: @@ -73,7 +81,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - run: pip install -r requirements.txt - run: pytest --cov=app ``` @@ -89,16 +97,21 @@ If even one test fails, the pipeline goes red and the PR is blocked (if you turn ## What you get -- **Errors caught immediately**, not a week later in production. -- **Tests always run**: no relying on "I'll run them locally before merge". -- **Pull requests show** whether tests passed; reviewers see it right away. -- **Coverage is tracked**: if a PR adds code without tests, the report shows it. +- **Errors caught immediately**, not a week later in production. +- **Tests always run**: no relying on "I'll run them locally before merge". +- **Pull requests show** whether tests passed; reviewers see it right away. +- **Coverage is tracked**: if a PR adds code without tests, the report shows it. -## What's next? +## Understanding check -That wraps up the testing module. You can write pytest tests, use fixtures and parametrization, mock dependencies, read `unittest` code in legacy projects, measure coverage, and set up CI. That's enough to jump into any production project; the rest you'll pick up on the job. +**What is true about coverage and CI?** ---- +1. **Correct answer:** Coverage shows what percentage of source code is executed during tests β€” Correct. It's a signal about gaps in testing, but not a guarantee that the code itself is correct. -**What is true about coverage and CI?** +2. For pytest integration you use coverage.py directly β€” coverage.py is the underlying library, but for pytest integration you usually install the pytest-cov wrapper plugin. + +3. CI is only for running tests on a schedule β€” CI runs tests automatically on every push/PR. Scheduled runs are a separate use case. +4. 100% coverage guarantees no bugs β€” No. You can cover a line but not check it well, or miss an edge case. High coverage is a useful signal, not a correctness guarantee. + +That wraps up the testing module. You can write pytest tests, use fixtures and parametrization, mock dependencies, read `unittest` code in legacy projects, measure coverage, and set up CI. That's enough to jump into any production project; the rest you'll pick up on the job. diff --git a/course/module-3/intro-concurrency-async/article.md b/course/module-3/intro-concurrency-async/article.md index f891159..aa1adab 100644 --- a/course/module-3/intro-concurrency-async/article.md +++ b/course/module-3/intro-concurrency-async/article.md @@ -1,28 +1,61 @@ +--- +meta: + title: "Concurrency, parallelism and asynchrony in Python" + description: "Three ways to run tasks seemingly at once: threads, processes, asyncio. What GIL is and which tool to pick when." +--- + # Concurrency, parallelism and asynchrony in Python Suppose our program needs to download 100 pages from different sites. Done one by one, each request waits 1-2 seconds for the server, and the total time is the sum of all those waits. Most of that time the CPU sits idle. The fix is obvious: while one request waits, send the others. That's **concurrent execution**. Python has three tools for this: `threading`, `multiprocessing`, `asyncio`. This article is the map: which to use when. +First, let's see the problem for real. Three "downloads" one after another, each waiting half a second: + +```python +import time + +def download(url): + time.sleep(0.5) # simulating the wait for a server response + return f"data from {url}" + +start = time.time() +for url in ["site-1", "site-2", "site-3"]: + print(download(url)) + +data from site-1 +data from site-2 +data from site-3 + +print(f"Took {time.time() - start:.1f} s") + +Took 1.5 s + +``` + +Three requests β€” a second and a half, and the CPU did nothing the whole time, just waited for responses. Next, three different ways to fit those same requests into half a second. + ## Three concepts People mix up three similar words when talking about concurrency. The distinction matters: -- **Concurrency**: tasks can switch among themselves, creating the illusion of simultaneous work. One barista at the counter takes an order, starts the milk heating, moves to the next customer, comes back to the milk. One worker, several tasks "in the air" at once. +- **Concurrency**: tasks can switch among themselves, creating the illusion of simultaneous work. One barista at the counter takes an order, starts the milk heating, moves to the next customer, comes back to the milk. One worker, several tasks "in the air" at once. -- **Parallelism**: tasks run **physically at the same time** on different CPU cores. Several baristas, each making their own coffee. Requires multiple cores. +- **Parallelism**: tasks run **physically at the same time** on different CPU cores. Several baristas, each making their own coffee. Requires multiple cores. -- **Asynchrony**: a code organization style where a task can be "deferred" while waiting (for example, for a server reply) without blocking the entire thread. It's a **way to achieve concurrency** on a single thread, without OS-level switching. +- **Asynchrony**: a code organization style where a task can be "deferred" while waiting (for example, for a server reply) without blocking the entire thread. It's a **way to achieve concurrency** on a single thread, without OS-level switching. Concurrency is the goal; parallelism and asynchrony are two ways to achieve it. +![Illustration: three timelines β€” parallelism (two tasks at once on two cores), concurrency (one timeline with alternating tasks), asynchrony (one timeline where await pauses are filled by other tasks)](https://python-academy.org/static/guidePage/intro-concurrency-async/concurrency-models-en.webp "Three concurrency models: parallelism, task switching, asynchrony") + ## I/O-bound vs CPU-bound: the key dichotomy The choice of tool depends **only** on what your task is waiting for: -- **I/O-bound**: the CPU sits idle while an external resource responds. Network request, disk read, database reply. Asynchrony wins here: while one request waits, fire the next ones. +- **I/O-bound**: the CPU sits idle while an external resource responds. Network request, disk read, database reply. Asynchrony wins here: while one request waits, fire the next ones. -- **CPU-bound**: the CPU is genuinely busy with computation. Image compression, encryption, scientific computing. Real parallelism across cores is what helps. +- **CPU-bound**: the CPU is genuinely busy with computation. Image compression, encryption, scientific computing. Real parallelism across cores is what helps. The most common beginner mistake: reaching for `multiprocessing` to download pages, or `asyncio` to multiply matrices. That gives a slowdown, not a speedup. @@ -32,25 +65,30 @@ CPython (the standard implementation) has a **Global Interpreter Lock (GIL)**, a The consequence: -- For **CPU-bound** tasks, `threading` is useless: threads share a single core through the GIL. You need processes (`multiprocessing`); each has its own GIL and its own core. -- For **I/O-bound** tasks the GIL is **released** while waiting for network/disk. So `threading` works great for I/O, as does `asyncio` (but without the overhead of OS threads). +- For **CPU-bound** tasks, `threading` is useless: threads share a single core through the GIL. You need processes (`multiprocessing`); each has its own GIL and its own core. +- For **I/O-bound** tasks the GIL is **released** while waiting for network/disk. So `threading` works great for I/O, as does `asyncio` (but without the overhead of OS threads). ## Which tool when -| Task | Tool | -| --- | --- | -| Many network requests, thousands of connections | **asyncio** | -| I/O in legacy code without async libraries | **threading** | -| Heavy computation on multiple cores | **multiprocessing** | -| Simple parallelization without diving in | **concurrent.futures** (`ThreadPoolExecutor` / `ProcessPoolExecutor`) | +| Task | Tool | +| ----------------------------------------------- | --------------------------------------------------------------------- | +| Many network requests, thousands of connections | **asyncio** | +| I/O in legacy code without async libraries | **threading** | +| Heavy computation on multiple cores | **multiprocessing** | +| Simple parallelization without diving in | **concurrent.futures** (`ThreadPoolExecutor` / `ProcessPoolExecutor`) | The next articles cover each in turn. First **threads and processes**, then **asyncio** (two articles: basics and advanced). -## What's next? +## Understanding check -The next article covers **threads and processes**: the two classic approaches. A thread is a lightweight worker inside a single process (for I/O); a process is a separate program with its own memory (for CPU-bound). +**Which of the following is true about concurrency in Python?** ---- +1. Parallelism and asynchrony are synonyms β€” They're different concepts. Parallelism is about physically simultaneous execution (needs multiple cores). Asynchrony is about switching tasks on a single thread while waiting for I/O. -**Which statement most accurately describes the difference between parallelism and asynchrony?** +2. **Correct answer:** Parallelism is physically simultaneous execution on multiple cores; asynchrony is task switching on a single thread during waits β€” Parallelism needs multicore hardware and helps CPU-bound work. Asynchrony works effectively on a single thread for I/O-bound work. +3. Asynchrony always requires a multicore CPU β€” The opposite: asynchrony works just fine on a single core, switching between tasks while they wait. Multicore is required for true parallelism. + +4. Python's GIL makes threading useless for all kinds of tasks β€” The GIL only limits CPU-bound work. For I/O-bound work, threads are effective because the GIL is released while waiting for network/disk. + +The next article covers **threads and processes**: the two classic approaches. A thread is a lightweight worker inside a single process (for I/O); a process is a separate program with its own memory (for CPU-bound). diff --git a/course/module-3/intro-to-databases/article.md b/course/module-3/intro-to-databases/article.md index 03564b5..c52079b 100644 --- a/course/module-3/intro-to-databases/article.md +++ b/course/module-3/intro-to-databases/article.md @@ -1,110 +1,70 @@ -# Database Fundamentals in Python: What are Databases and Why We Need Them +--- +meta: + title: "What a Database Is and Why You Need One" + description: "Why a Python list is not enough for storing application data, what a database and a DBMS are, and the main types of databases." +--- -Every day we use dozens of applications: checking social media, -shopping online, listening to music, booking tickets. -Behind each of them is a **database** β€” a system that instantly finds the right information among millions of records. +# What a Database Is and Why You Need One -**Databases** are the fundamental technology of modern applications. -For Python developers, understanding database principles opens doors to creating scalable and reliable solutions. +Almost any application stores something for the long term: users, orders, messages. The first idea is to keep them in a familiar Python list. Let's see where that idea breaks down. -## What is a Database? - -> **Database (DB)** β€” an organized collection of data that is stored and managed by a specialized system (DBMS). - -Simple analogy: a database is like a smart warehouse. Each "shelf" has its own address, and the "warehouse robot" (DBMS) can quickly find and deliver any needed information. - -## Why Not Store Data in Python Lists? - -Let's consider a simple example β€” storing users: +## Why not keep the data in a Python list ```python -# Storage in Python list users = [ {"id": 1, "name": "Anna", "email": "anna@example.com"}, - {"id": 2, "name": "Peter", "email": "peter@example.com"}, + {"id": 2, "name": "Peter", "email": "petr@example.com"}, # ... imagine a million users here ] -# Search user by email def find_user_by_email(email): - for user in users: # Check the ENTIRE list! 😱 + for user in users: # scanning the whole list if user["email"] == email: return user return None ``` -**Problems with this approach:** - -- **Slow search** β€” need to check every user -- **Memory limitations** β€” all data must fit in RAM -- **Data loss** β€” if the program crashes, everything is lost -- **Concurrency** β€” what if multiple users modify data simultaneously? +With a thousand users everything works. Then the problems begin: -**Databases solve all these problems out of the box** β€” that's why they became the standard for serious applications. +- **Slow search** β€” "find by email" walks through the records one by one, and at a million that's a noticeable delay. +- **Everything in RAM** β€” the data must fit into memory entirely. +- **Data lives until restart** β€” the program exits, and the list is gone. +- **Concurrent access** β€” two people change the data at the same moment, and a list isn't ready for that. -## Main Types of Databases +## What a database is -There are many types of databases, but let's highlight the three most important: +These are exactly the problems a **database** solves: fast search by the field you need, storage on disk between runs, and concurrent access without confusion. -### 1. Relational Databases +> A **database (DB)** is an organized collection of data managed by a dedicated program β€” a DBMS (database management system). -Organize data into **tables** with clear structure. The most popular type. +A simple analogy: a database is a smart warehouse. Every "shelf" has its own address, and the "warehouse robot" (the DBMS) quickly finds and hands over whatever is needed. -```sql --- Example users table -CREATE TABLE users ( - id INTEGER PRIMARY KEY, - name VARCHAR(100), - email VARCHAR(150) -); -``` +## The main types of databases -### 2. Key-Value Databases +There are many kinds of databases, but in practice three keep coming up: -Store simple "key-value" pairs. Very fast for simple operations. +| Database type | Description | +| ----------------- | ---------------------------------------- | +| Relational | Data in linked tables β€” rows and columns | +| Key-value | Fast access to a value by its key | +| Document-oriented | Flexible documents in JSON format | -```python -# Example in Redis β€” popular key-value DB -user_session = { - "session:user123": "logged_in", - "cart:user123": "[1,5,9]", # Product IDs in cart - "last_seen:user123": "2024-01-15 10:30" -} -``` +## The most common DBMSs -### 3. Document-Oriented Databases +Relational databases are what you'll encounter most: **PostgreSQL** and **MySQL** on servers, **SQLite** β€” inside phones, browsers and Python itself. Of the rest, **Redis** (key-value: caches and sessions) and **MongoDB** (documents) come up regularly. -Store data as documents (JSON format). Flexible structure. +The next three chapters are about relational databases, so from here on we talk only about them. -```python -# Example user document -{ - "name": "Anna", - "email": "anna@example.com", - "preferences": { - "theme": "dark", - "language": "en" - } -} -``` +## Understanding check -## Popular Database Management Systems +**Why do applications store their users in a database rather than a plain list in memory?** -Here's the top-8 most used DBMS in 2024: +1. **Correct answer:** A database searches quickly by the field you need, keeps data between runs, and allows concurrent access β€” Fast search by the right field instead of scanning the whole list, data that survives a restart, and several clients writing at once without confusion β€” those are exactly the jobs a database exists for. -1. **PostgreSQL** β€” powerful **relational** DB -2. **MySQL** β€” popular **relational** DB for web -3. **SQLite** πŸ“± β€” lightweight **relational** DB -4. **MongoDB** β€” leader in **document-oriented** DBs -5. **Redis** ⚑ β€” fast **key-value** DB -6. **Oracle Database** β€” enterprise **relational** DB -7. **Microsoft SQL Server** β€” **relational** DB from Microsoft -8. **Elasticsearch** β€” **document-oriented** search DB +2. A database is always faster than any Python code β€” Speed depends on the task: accessing a single element by index is even faster with an in-memory list. The database wins at searching large volumes, reliability and concurrent access. -**Conclusion:** relational databases (5 out of 8 positions) are the industry standard -due to their reliability and universality. +3. A Python list cannot hold more than 1000 elements β€” List size is limited only by RAM, not by the number 1000. The problem is not an element limit but search speed and losing data on restart. -## What's Next? +4. With a database you don't need to write code β€” You still write queries β€” in SQL or through a library. A database saves you not from code but from manual searching, file-based storage and fighting over concurrent access. -In the next article, we'll start practicing with **SQLite** β€” the perfect database for learning, which is already built into Python. -You'll create your first DB and learn to work with real data. +In the next article we start practicing with **SQLite** β€” a database that's ideal for learning and already built into Python: we'll create our first DB and work with real data. diff --git a/course/module-3/intro-to-testing/article.md b/course/module-3/intro-to-testing/article.md index beefd24..7b4db4a 100644 --- a/course/module-3/intro-to-testing/article.md +++ b/course/module-3/intro-to-testing/article.md @@ -1,119 +1,114 @@ -# Testing Fundamentals in Python: What, Why, and Types of Tests +--- +meta: + title: "Why Tests Matter and What Kinds Exist" + description: "Why bugs are cheaper to catch early, the types of tests (unit, integration, E2E), the testing pyramid and the FIRST principles." +--- -Imagine you're building a bridge β€” would you let people cross it without first checking how safe it is? +# Why tests matter and what kinds exist -The same principle applies to your code! Testing helps ensure your programs work as expected, even when you make changes or add new features. +You fixed one function, shipped the update β€” and a day later a user writes that something else broke, something that worked yesterday. A change in one place quietly affected another, and you were the last to find out. -## What is Software Testing? +Testing is how you learn about breakages like this right away, not from users. It's a set of checks that confirm your code does what it's expected to β€” and keeps doing it after every change. -> **Software testing** is the process of investigating and evaluating a software product to verify that it meets the specified requirements and to identify potential defects (errors or bugs) before end-users encounter them. +## Why test at all -Simply put, it's a way to make sure your code does what it's supposed to do and doesn't do what it's not supposed to do. +In 1999, NASA lost a $125 million Mars orbiter because of a simple mistake: the calculations mixed different unit systems. A single test could have caught the mismatch before launch. -## Why is Testing Necessary? Key Benefits +That's an extreme case of a general rule β€” the later a bug is found, the more it costs to fix: -Have you ever sent a message with an embarrassing typo? 😳 Imagine if your code had the same problem, but instead of slight embarrassment, it led to serious consequences! +| When the bug is found | Relative cost of fixing | +| ----------------------- | ----------------------- | +| During development | 1x | +| During code review | 2x | +| During QA testing | 5x-10x | +| After release (in prod) | 30x-100x+ | -### 1. Early Bug Detection and Reduced Cost of Fixing +Beyond catching bugs early, tests give you a few more things: -In 1999, NASA lost a \$125 million Mars orbiter due to a simple calculation error involving different unit systems. A simple test could have detected this discrepancy before launch πŸš€. +- **confidence when changing code** β€” with a test suite you can refactor and add features boldly: if something breaks, the tests are the first to tell you; +- **living documentation** β€” tests show how the code is meant to be used and, unlike ordinary docs, never go stale; +- **better design** β€” if a function is hard to test, it's usually just built too complex; +- **protection from regressions** β€” that's the name for breakage of previously working code after new changes; the example below catches exactly those. -Software errors can range from minor inconveniences to catastrophic failures with huge financial losses, security issues, or damage to reputation. +## Types of tests -**The later a bug is found, the more expensive it is to fix:** +Tests differ in the scale of what they check β€” from a single function to the whole system. -| When Bug is Found | Relative Cost to Fix | -| -------------------------- | -------------------- | -| During Development | 1x | -| During Code Review | 2x | -| During QA Testing | 5x-10x | -| After Release (Production) | 30x-100x+ | +### Unit tests -Testing helps identify problems early, when fixing them requires less time and resources. +They check the smallest, isolated parts of the program β€” individual functions, methods or classes. The goal is to make sure every "brick" of the code works correctly on its own. -### 2. Confidence When Making Changes and Refactoring - -Have you ever been afraid to change existing code for fear of breaking something? Having a good set of tests gives you a "safety net." You can confidently refactor, add new features, or fix bugs, knowing that tests will alert you if something goes wrong. - -### 3. Tests as Living Documentation - -Well-written tests serve as a form of living, executable documentation. They clearly demonstrate how your code should be used and what behavior is expected from it. Unlike static documentation, tests don't become outdated because they must pass for every version of the code. - -### 4. Improving Code Design - -The process of writing tests often forces you to think about the structure and design of your code. If a function or module is difficult to test, it might be a sign that it's too complex, has too many dependencies, or violates the single responsibility principle. Testability is an important aspect of good design. - -### 5. Preventing Regressions - -Regressions are bugs that reappear in previously working code after changes have been made. Automated tests effectively catch such problems, ensuring that old functionality hasn't been accidentally broken. - -## Types of Tests - -There are several levels and types of tests, each serving its own purpose. +```python +def add(a, b): + return a + b -### 1. Unit Tests +print(add(2, 3) == 5) + +True + +``` -Unit tests check the smallest, isolated parts of your program β€” individual functions, methods, or classes. The goal is to ensure that each "building block" of your code works correctly on its own. +The check returned `True` β€” the function works as expected. Now imagine someone edited `add` and accidentally broke the logic: ```python -# Example: function for addition def add(a, b): - return a + b + return a + b + 1 # a bug that slipped in -# Simple manual test (concept) -# result = add(2, 3) -# expected = 5 -# print(f"Test passed: {result == expected}") -# Later we'll see how to automate this with frameworks +print(add(2, 3) == 5) + +False + ``` -### 2. Integration Tests +The same check instantly returned `False`. That's what a unit test is: a small check that tells you by itself whether the function is intact β€” that's how regressions get caught. In the next article we'll see how to write such checks with `assert` and run them in batches. -Integration tests check the interaction between multiple modules or components of the system. For example, how your order processing module interacts with the notification module or the database. They help ensure the "building blocks" fit together correctly. +### Integration tests -- **Example**: Verifying that after a user is successfully created in the database (one module), the authentication system (another module) can recognize them. +They check how several modules work together: for example, that after a user is created in the database, the login system recognizes them. Modules can be fine on their own and still fail to fit together β€” that's what gets caught here. -### 3. Functional / End-to-End (E2E) Tests +### End-to-end tests (E2E) -These tests check the entire system or a significant part of it from the user's perspective. They simulate real user scenarios, going through all layers of the application β€” from the user interface (if one exists) to the database. +They check the whole system from the user's point of view, passing through every layer of the application. An example is the full signup scenario: fill in the form, submit the data, receive the email, log in. -- **Example**: A complete scenario of registering a new user on a website: filling out the form, submitting data, receiving a confirmation email, logging in for the first time. +## The testing pyramid -## The Testing Pyramid +How many of each kind should you write? The usual answer is drawn as a pyramid: -The testing pyramid is a model that helps visualize the recommended ratio of different types of tests in a project. +| Level | Characteristics | +| ---------------- | --------------- | +| Unit | fast Β· many | +| Integration | in between | +| End-to-end (E2E) | slow Β· few | -![Description of the testing pyramid](https://python-academy.org/static/guidePage/intro-to-testing/en_testing.jpg 'Description of the testing pyramid') +The higher the level, the slower and more expensive the tests β€” so there are fewer of them. The bulk is fast unit tests: they point exactly at the place of the breakage. -**The idea of the pyramid:** +## Principles of a good test (FIRST) -- **Unit Tests** form the base. You should have the most of these, as they are fast, reliable, and pinpoint the location of errors accurately. -- **Integration Tests** are on the next level. There are fewer of them than unit tests, and they verify the interaction between components. -- **Functional (E2E) Tests** are at the top. You should have the fewest of these, as they are slow, brittle (often break due to UI changes), and expensive to maintain. +Five traits of a useful test spell the acronym FIRST: -Adhering to this structure helps create an effective and reliable testing strategy. +- **F**ast: slow tests get run rarely; +- **I**ndependent: doesn't rely on other tests or the run order; +- **R**epeatable: gives the same result on every run; +- **S**elf-validating: tells you by itself whether it passed, with no manual comparison of results; +- **T**imely: written together with the code, not "someday later". -## Principles of a Good Test (FIRST) +## Testing frameworks -For tests to be truly useful, they should adhere to certain principles. The acronym FIRST helps remember them: +Writing checks by hand, as in the `add` example, gets old fast: you want batch runs, reports, convenient comparisons. That's what frameworks are for, and Python has two main ones: -- **F**ast: Tests should run quickly. Slow tests slow down development and are run less often. -- **I**ndependent/Isolated: Tests should not depend on each other. The order in which they run should not affect the outcome. Each test should set up its own environment and clean up if necessary. -- **R**epeatable: Tests should produce the same result every time they are run in the same environment. No random failures or dependency on external mutable factors. -- **S**elf-Validating: The test should determine whether it passed or failed without requiring manual inspection of the results. This is usually achieved using `assert` statements. -- **T**imely/Thorough: Tests should be written in a timely manner (ideally, before or along with the code they test - TDD). They should be thorough enough to cover important aspects of the code under test, including edge cases. +- **pytest** β€” third-party, with a concise syntax; that's where we'll start; +- **unittest** β€” built into the standard library. -## Brief Introduction to Testing Frameworks +## Understanding check -While you can write checks manually, as in the `add` function example, this quickly becomes inefficient. Testing frameworks provide tools and structure for writing, organizing, and running tests, as well as for reporting results. +**Which statement about software testing is the most accurate?** -Popular frameworks in Python include: +1. **Correct answer:** Testing helps discover bugs at early stages of development, reducing the cost of fixing them. β€” The earlier a bug is caught, the cheaper the fix β€” up to a hundredfold difference between development and production. -- **`pytest`**: A powerful and flexible third-party framework known for its concise syntax. We will start our exploration with this one. -- **`unittest`**: Python's built-in module that follows the classic xUnit style. +2. The main goal of testing is to reach 100% code coverage. β€” Coverage is a useful metric (we'll talk about it later), but 100% coverage by itself doesn't guarantee the absence of bugs. Test quality matters more. -## What's Next? +3. Functional (E2E) tests are the fastest and should form the base of the testing pyramid. β€” According to the testing pyramid, the base is unit tests: they are the fastest and most numerous. End-to-end tests sit at the top of the pyramid. -In the next article, we will dive into the practical writing of tests using the `pytest` framework, which will help us easily and effectively automate the verification of our code. +4. The FIRST principle means tests should above all be Thorough, even if they are slow. β€” FIRST specifically stresses being Fast: slow tests get run rarely, and that makes them less useful. -**Which statement about software testing is most accurate?** +In the next article β€” hands-on practice with `pytest`: your first real tests, the `assert` statement and batch runs. diff --git a/course/module-3/iterations-and-loops/article.md b/course/module-3/iterations-and-loops/article.md deleted file mode 100644 index 1737997..0000000 --- a/course/module-3/iterations-and-loops/article.md +++ /dev/null @@ -1,331 +0,0 @@ -# Iterations and Loops - -> Iteration in Python is the process of sequentially traversing elements in a collection (such as a list, tuple, string). -> Loops are constructs that allow you to automate the iteration process and execute a block of code multiple times. - -## The for Loop - -The `for` loop in Python is used to iterate over sequences (lists, tuples, strings, and other iterable objects): - -```python -# Iterating through a list -fruits = ["apple", "banana", "orange"] -for fruit in fruits: - print(fruit) -# Output: -# apple -# banana -# orange - -# Iterating through a string -for char in "Python": - print(char) -# Output: -# P -# y -# t -# h -# o -# n -``` - -### The range() Function - -The `range()` function generates a sequence of numbers and is often used with the `for` loop to execute code a certain number of times: - -```python -# range(stop) - generates numbers from 0 to stop-1 -for i in range(5): - print(i) -# Output: -# 0 -# 1 -# 2 -# 3 -# 4 - -# range(start, stop) - generates numbers from start to stop-1 -for i in range(2, 6): - print(i) -# Output: -# 2 -# 3 -# 4 -# 5 - -# range(start, stop, step) - generates numbers from start to stop-1 with step increment -for i in range(1, 10, 2): - print(i) -# Output: -# 1 -# 3 -# 5 -# 7 -# 9 -``` - -### The enumerate() Function - -The `enumerate()` function allows you to get the index of an element along with the element itself: - -```python -fruits = ["apple", "banana", "orange"] -for index, fruit in enumerate(fruits): - print(f"Index: {index}, Value: {fruit}") -# Output: -# Index: 0, Value: apple -# Index: 1, Value: banana -# Index: 2, Value: orange - -# You can set the starting index -for index, fruit in enumerate(fruits, 1): - print(f"Number {index}: {fruit}") -# Output: -# Number 1: apple -# Number 2: banana -# Number 3: orange -``` - -### Iterating Over Multiple Sequences with zip() - -The `zip()` function allows you to iterate over multiple sequences simultaneously: - -```python -names = ["John", "Maria", "Alex"] -ages = [25, 30, 22] - -for name, age in zip(names, ages): - print(f"{name}: {age} years old") -# Output: -# John: 25 years old -# Maria: 30 years old -# Alex: 22 years old -``` - -## The while Loop - -The `while` loop executes a block of code as long as a given condition remains true: - -```python -# Basic while loop -count = 0 -while count < 5: - print(count) - count += 1 -# Output: -# 0 -# 1 -# 2 -# 3 -# 4 - -# Using a condition with user input -password = "" -while password != "secret": - password = input("Enter the password: ") -print("Correct password!") -``` - -### Infinite Loops - -If the condition in a `while` loop is always true, the loop will run infinitely. To exit such a loop, the `break` operator or exceptions are used: - -```python -# Infinite loop with a conditional exit -while True: - user_input = input("Enter 'exit' to quit: ") - if user_input.lower() == "exit": - break - print(f"You entered: {user_input}") -``` - -## The break, continue, and else Operators in Loops - -### The break Operator - -The `break` operator allows you to exit a loop prematurely: - -```python -# Searching in a list with exit upon finding -fruits = ["apple", "banana", "orange", "pear"] -for fruit in fruits: - if fruit == "orange": - print("Found an orange!") - break - print(f"Checking: {fruit}") -# Output: -# Checking: apple -# Checking: banana -# Found an orange! -``` - -### The continue Operator - -The `continue` operator skips the remaining code in the current iteration and moves to the next one: - -```python -# Skipping odd numbers -for i in range(1, 6): - if i % 2 != 0: # If the number is odd - continue - print(i) -# Output: -# 2 -# 4 -``` - -### The else Block in Loops - -Both `for` and `while` can have an optional `else` block that executes if the loop completes normally (without a `break`): - -```python -# Checking if a number is prime -num = 13 -for i in range(2, num): - if num % i == 0: - print(f"{num} is not a prime number") - break -else: - print(f"{num} is a prime number") -# Output: -# 13 is a prime number - -# Example with a while loop -count = 0 -while count < 3: - print(count) - count += 1 -else: - print("The while loop completed normally") -# Output: -# 0 -# 1 -# 2 -# The while loop completed normally -``` - -## List Comprehensions - -List comprehensions are a compact way to create lists from other sequences: - -```python -# Traditional way to create a list -squares = [] -for i in range(1, 6): - squares.append(i ** 2) -print(squares) # [1, 4, 9, 16, 25] - -# The same using a list comprehension -squares = [i ** 2 for i in range(1, 6)] -print(squares) # [1, 4, 9, 16, 25] - -# List comprehension with a condition -even_squares = [i ** 2 for i in range(1, 11) if i % 2 == 0] -print(even_squares) # [4, 16, 36, 64, 100] - -# List comprehension with multiple conditions (branching) -numbers = [i if i % 2 == 0 else -i for i in range(1, 6)] -print(numbers) # [-1, 2, -3, 4, -5] -``` - -## Dictionary and Set Comprehensions - -Similar to list comprehensions, you can create dictionary and set comprehensions: - -```python -# Dictionary comprehension -word = "hello" -char_positions = {char: index for index, char in enumerate(word)} -print(char_positions) # {'h': 0, 'e': 1, 'l': 3, 'o': 4} - -# Set comprehension -vowels = "aeiou" -consonants = {char for char in "python" if char not in vowels} -print(consonants) # {'p', 'y', 't', 'h', 'n'} -``` - -## Generator Expressions - -Generator expressions are similar to list comprehensions but use parentheses instead of square brackets and generate values "on the fly," which saves memory: - -```python -# List comprehension (creates the entire list in memory) -list_comp = [i ** 2 for i in range(1000000)] -# Takes up a lot of memory - -# Generator expression (generates values on demand) -gen_exp = (i ** 2 for i in range(1000000)) -# Takes minimal memory - -# Using a generator expression with sum() -total = sum(i ** 2 for i in range(1, 6)) -print(total) # 55 (1 + 4 + 9 + 16 + 25) -``` - -## Iterating Over Dictionaries - -There are several ways to iterate over dictionaries: - -```python -person = { - "name": "John", - "age": 30, - "city": "New York" -} - -# Iterating over keys (default) -for key in person: - print(key, person[key]) - -# Explicitly iterating over keys -for key in person.keys(): - print(key) - -# Iterating over values -for value in person.values(): - print(value) - -# Iterating over key-value pairs -for key, value in person.items(): - print(f"{key}: {value}") -``` - -## Nested Loops - -Loops can be nested within each other: - -```python -# Nested loops to create a multiplication table -for i in range(1, 4): - for j in range(1, 4): - print(f"{i} * {j} = {i * j}") - print("-----") -# Output: -# 1 * 1 = 1 -# 1 * 2 = 2 -# 1 * 3 = 3 -# ----- -# 2 * 1 = 2 -# 2 * 2 = 4 -# 2 * 3 = 6 -# ----- -# 3 * 1 = 3 -# 3 * 2 = 6 -# 3 * 3 = 9 -# ----- -``` - -## Independent Exercises - -1. Write a program that asks the user for numbers until they enter 0, and then outputs the sum of all entered numbers. -2. Create a list comprehension that generates a list of squares of even numbers from 1 to 20. -3. Write a program that prints all numbers from 1 to 100, but instead of numbers divisible by 3, it prints "Fizz", instead of numbers divisible by 5, it prints "Buzz", and instead of numbers divisible by both 3 and 5, it prints "FizzBuzz". -4. Using nested loops, create a program that prints the following pattern: - ``` - * - ** - *** - **** - ***** - ``` -5. Create a dictionary where the keys are numbers from 1 to 10, and the values are their cubes, using a dictionary comprehension. diff --git a/course/module-3/mocks-and-stubs/article.md b/course/module-3/mocks-and-stubs/article.md index 5710023..c45f6e9 100644 --- a/course/module-3/mocks-and-stubs/article.md +++ b/course/module-3/mocks-and-stubs/article.md @@ -1,13 +1,23 @@ +--- +meta: + title: "Mocks and stubs: isolating tests from external dependencies" + description: "Why you need mocks and stubs in Python, how to use unittest.mock.patch, and basic unittest.mock.Mock usage." +--- + # Mocks and stubs: isolating tests -Real code talks to the outside world a lot: HTTP requests, databases, files, time. That's a problem in tests: the network can fail, the DB can be slow, time is uncontrollable. A test should check **your logic**, not whether someone else's service is up. +The same payment test sometimes passes instantly, sometimes hangs for ten seconds, and occasionally fails with a timeout β€” though nobody touched the code. The reason: every run it reaches out to the payment provider's real sandbox, which is fast one minute and slow the next. In effect the test is checking someone else's server, not **your logic**. + +That's what happens when code under test talks to the outside world: HTTP requests, databases, files, time. The network drops, the DB is slow, time is uncontrollable β€” so for the duration of the test the dependency has to be replaced with something. The fix is a **test double**: slip a fake object into your code that behaves the way you want. The fake has two main roles: -- **Stub** returns canned answers, no checks needed. -- **Mock** does the same and also records calls (with which arguments, how many times) so the test can verify them. +- **Stub** returns canned answers, no checks needed. +- **Mock** does the same and also records calls (with which arguments, how many times) so the test can verify them. + +Both are built with one tool β€” `unittest.mock` from the standard library. The name is historical: the module shipped alongside the built-in `unittest` framework (we'll get to it one article from now), but it's tied to pytest just as much β€” nothing to install, it works as is. -In Python both are built with one tool: `unittest.mock`. +![Illustration: on the left a Mock with a green check and charge.return_value = True, passed as an argument into the test process_payment(service, 100) in the centre, while the real payment_service on the right is crossed out in red (slow, unreliable)](https://python-academy.org/static/guidePage/mocks-and-stubs/mock-substitution-en.webp "A Mock is passed as an argument in place of the real service") ## The Mock object @@ -35,9 +45,31 @@ def test_payment_success(): `process_payment` takes `service` as an argument. In production code that's the real payment service; in the test we pass a Mock through the same argument β€” this is the point of "dependency through a parameter". Then: -- `Mock()` creates an object where **any** attribute or method exists automatically. -- `service.charge.return_value = True` says "when the test calls `service.charge(...)`, return `True`". -- `assert_called_once_with(100)` checks: "method `charge` was called exactly once with the argument `100`". That's the difference between a Mock and a Stub: the test doesn't just get data, it verifies **how** the code used the dependency. +- `Mock()` creates an object where **any** attribute or method exists automatically. +- `service.charge.return_value = True` says "when the test calls `service.charge(...)`, return `True`". +- `assert_called_once_with(100)` checks: "method `charge` was called exactly once with the argument `100`". That's the difference between a Mock and a Stub: the test doesn't just get data, it verifies **how** the code used the dependency. + +A mock is easier to touch once than to describe. Run this β€” and look at the last line: a `Mock` even has a method we never defined: + +```python +from unittest.mock import Mock + +m = Mock() +m.charge.return_value = True + +print(m.charge(100)) + +True + +print(m.charge.call_args) + +call(100) + +print(type(m.method_that_does_not_exist).__name__) + +Mock + +``` ## patch: replace an already existing object @@ -72,9 +104,9 @@ def test_get_user(mock_get): A few things to note: -- `@patch("requests.get")` swaps `requests.get` for a mock only for the duration of the test. After the test, everything is restored. -- The `mock_get` argument (any name works) is the auto-created mock that replaced the original. You configure behaviour and check calls on it. -- `response.json()` is a **method** (with parentheses), so we configure `mock_response.json.return_value`. The nested mock `json` has its own `return_value`. Any attribute or method of a mock is itself a mock β€” it chains. +- `@patch("requests.get")` swaps `requests.get` for a mock only for the duration of the test. After the test, everything is restored. +- The `mock_get` argument (any name works) is the auto-created mock that replaced the original. You configure behaviour and check calls on it. +- `response.json()` is a **method** (with parentheses), so we configure `mock_response.json.return_value`. The nested mock `json` has its own `return_value`. Any attribute or method of a mock is itself a mock β€” it chains. `patch` also works as a **context manager** (`with patch(...) as mock_get:`), handy when the swap is needed only for part of the test. @@ -141,11 +173,16 @@ Mocks are convenient but tricky: it's easy to start mocking the **internals** of Rule: **mock the boundaries of the system**: external APIs, the DB, the filesystem, time. Test your own code directly, without mocks. -## What's next? +## Understanding check -Next: the built-in **`unittest`** module, classic xUnit style with `TestCase` classes and `setUp`/`tearDown`. It's the alternative to pytest you'll meet in legacy code. +**What is true about mocks and stubs in Python?** ---- +1. A Stub verifies that methods were called with the right arguments β€” Stubs just return canned data. Verifying calls is the job of mocks. -**What is true about mocks and stubs in Python?** +2. **Correct answer:** patch from unittest.mock temporarily replaces an object with a mock during the test β€” Once the test finishes, patch puts the original object back. + +3. side_effect is only for returning sequential values β€” side_effect can also raise exceptions (e.g. ConnectionError) or call an arbitrary function. +4. In patch you give the path where the object is defined, not where it is used β€” The opposite: you patch where the object is looked up at call time β€” the path in the module that uses it, not where it was originally defined. This is the most common mistake with patch. + +Next: the built-in **`unittest`** module, classic xUnit style with `TestCase` classes and `setUp`/`tearDown`. It's the alternative to pytest you'll meet in legacy code. diff --git a/course/module-3/pytest-basics/article.md b/course/module-3/pytest-basics/article.md index 62f5f55..bb93d54 100644 --- a/course/module-3/pytest-basics/article.md +++ b/course/module-3/pytest-basics/article.md @@ -1,6 +1,12 @@ +--- +meta: + title: "pytest basics: your first tests in Python" + description: "Basic pytest usage: install, write tests with assert, run them and read the output. Minimum to get started." +--- + # pytest basics: your first tests in Python -`pytest` is the most popular Python testing framework. Its entry point is simple: you write regular Python functions with `assert`, then run `pytest` from the terminal. It auto-discovers your tests and reports what passed and what failed. +After every change you want to be sure nothing broke: you run an example, look at the output, check another one. By the fifth time it gets old β€” and that chore is exactly what `pytest`, the most popular Python testing framework, takes over. A test for it is a plain function with `assert`: you run `pytest` in the terminal, and it finds the tests itself and reports what passed and what failed. ## Install @@ -10,20 +16,22 @@ pip install pytest ## Your first test +`assert` is a Python operator, not part of pytest. The statement `assert expression` does nothing if the expression is true, and raises `AssertionError` if it's false. That's what pytest stands on: a test is an ordinary function that either runs quietly to the end or fails on the first `assert` that doesn't hold. + Put production code in `example.py`, and put tests in a file next to it with a `test_` prefix. pytest finds tests by that prefix automatically: - + +``` A test is just a function whose name starts with `test_`, with regular Python `assert` inside. No special classes, no inheritance, no registration. +![Illustration: a project folder with files app.py, test_math.py, test_strings.py, helpers.py. pytest scans the folder and finds files with the test_ prefix, skipping the others](https://python-academy.org/static/guidePage/pytest-basics/pytest-discovery-en.webp "pytest auto-discovers test_*.py files and test_* functions") + ## Running In the terminal, in the directory with your tests: @@ -91,12 +99,27 @@ test_example.py:9: AssertionError pytest shows: -- **where** it failed (`test_example.py:9: AssertionError`) -- **which expression** broke (`assert add(5, -2) == 10`) -- **what came out instead** (`3 == 10`, and that `3 = add(5, -2)`) +- **where** it failed (`test_example.py:9: AssertionError`) +- **which expression** broke (`assert add(5, -2) == 10`) +- **what came out instead** (`3 == 10`, and that `3 = add(5, -2)`) That detailed output usually tells you the cause immediately. This is the main argument for plain `assert` over special methods: pytest inspects the expression and shows the interesting parts. +## Checking that code fails + +A test captures not only the right answer but the right error. The call `add("a", 1)` should fail with a `TypeError` β€” that's behavior too, and worth pinning down. A plain `assert` can't check it: the exception would abort the test before the check is reached. That's what `pytest.raises` is for: + +```python +import pytest +from example import add + +def test_add_string_and_number_raises(): + with pytest.raises(TypeError): + add("a", 1) +``` + +The test passes if a `TypeError` is raised inside the `with` block, and fails if no exception occurs. + ## Test structure: Arrange / Act / Assert As tests get bigger, a pattern that hugely helps readability is **AAA (Arrange / Act / Assert)**: @@ -123,17 +146,22 @@ A simple test like `assert add(1, 2) == 3` fits on one line, and AAA isn't neede In real projects, the test name reads as a short statement about the code. A handy template: **`test___`**: -- `test_add_returns_sum_for_positive_numbers` β€” clearer than `test_add_1` -- `test_withdraw_fails_when_balance_is_zero` β€” instantly tells you what and why -- `test_user_email_is_lowercased_after_save` β€” pinpoints the behaviour +- `test_add_returns_sum_for_positive_numbers` β€” clearer than `test_add_1` +- `test_withdraw_fails_when_balance_is_zero` β€” instantly tells you what and why +- `test_user_email_is_lowercased_after_save` β€” pinpoints the behaviour When such a test fails in CI, the name alone tells you **what broke**, without reading the body. That saves hours of debugging in large projects. -## What's next? +## Understanding check -The next article covers pytest's two superpowers: **fixtures** (shared test setup) and **parametrization** (one test, many inputs). +**Which statement about pytest basics is correct?** ---- +1. You always have to pass test file names to pytest explicitly β€” pytest auto-discovers files starting with test\_ and functions starting with test\_. No need to list them by hand. -**Which statement about pytest basics is correct?** +2. When a test fails, pytest just prints FAILED without details β€” It does the opposite: pytest shows the expression, the actual values and the line, so you understand the cause quickly. +3. **Correct answer:** pytest uses regular Python assert for checks β€” Correct. No special methods like self.assertEqual(), just plain assert, and pytest introspects it for nice output. + +4. All tests in pytest must be class methods β€” Not required. Tests in pytest are top-level functions. Classes are optional for grouping. + +The next article covers pytest's two superpowers: **fixtures** (shared test setup) and **parametrization** (one test, many inputs). diff --git a/course/module-3/pytest-fixtures-parametrization/article.md b/course/module-3/pytest-fixtures-parametrization/article.md index b86f9d9..7f3e385 100644 --- a/course/module-3/pytest-fixtures-parametrization/article.md +++ b/course/module-3/pytest-fixtures-parametrization/article.md @@ -1,6 +1,12 @@ +--- +meta: + title: "pytest: fixtures and parametrization" + description: "pytest fixtures for shared setup via @pytest.fixture with yield, and parametrization via @pytest.mark.parametrize. The two features you reach for daily." +--- + # pytest: fixtures and parametrization -In the previous article we wrote simple test functions. Now we'll cover two things that keep tests from turning into copy-paste: **fixtures** (shared setup for many tests) and **parametrization** (one test, many inputs). +A dozen tests, each beginning with the same three lines: build a user, fill in the fields, open a connection. A new field gets added to the project β€” and you edit those lines in every test, missing one, of course. That's the copy-paste that rots a test suite. Two things in this article remove it: **fixtures** (shared setup for many tests) and **parametrization** (one test, many inputs). ## Fixtures: shared setup @@ -47,6 +53,8 @@ def test_read(temp_file): Code **before** `yield` runs before the test (setup); code **after** runs after the test (teardown). Works even if the test fails. +![Illustration: setup (code before yield) β†’ test β†’ teardown (code after yield); three blocks linked by arrows](https://python-academy.org/static/guidePage/pytest-fixtures-parametrization/fixture-lifecycle-en.webp "yield fixture: code before yield = setup, code after yield = teardown") + ## Scope: how often to recreate By default, a fixture runs **per test**. If setup is expensive (open a DB, load a big file), tell pytest to do it "once per session" with `scope="session"`: @@ -66,26 +74,24 @@ In practice, 95% of the time you'll use `function` (default, one instance per te If a fixture is needed in multiple files, put it in `conftest.py` next to your tests. No import needed, pytest finds it automatically: - + +``` That's the standard way to share fixtures across the tests of a project. @@ -131,11 +137,16 @@ test_discount.py::test_get_discount[30-True-0.1] PASSED If one case fails, the test name shows its parameters, so it's immediately clear which combination broke. -## What's next? +## Understanding check -Next up: **mocks and stubs**, how to isolate tests from external dependencies (DB, API, time) so they stay fast and predictable. +**What is true about fixtures and parametrization in pytest?** ---- +1. A fixture with scope="function" runs once per whole session β€” The opposite: scope="function" is the default and the fixture runs for every test. Once per session is scope="session". -**What is true about fixtures and parametrization in pytest?** +2. **Correct answer:** conftest.py lets you share fixtures across files without importing β€” pytest auto-discovers fixtures in conftest.py for tests in that directory and its subdirectories. + +3. @pytest.mark.parametrize sets the scope of a fixture β€” No, parametrize runs a test with different inputs. Fixture scope is set with @pytest.fixture(scope=...). +4. yield in a fixture only returns a value, nothing else β€” It returns the value, but the code AFTER yield runs as teardown, once the test is done. + +Next up: **mocks and stubs**, how to isolate tests from external dependencies (DB, API, time) so they stay fast and predictable. diff --git a/course/module-3/sqlalchemy-core-basics/article.md b/course/module-3/sqlalchemy-core-basics/article.md index 6219d1f..6336ef7 100644 --- a/course/module-3/sqlalchemy-core-basics/article.md +++ b/course/module-3/sqlalchemy-core-basics/article.md @@ -1,12 +1,35 @@ +--- +meta: + title: "SQLAlchemy Core: SQL from Python expressions" + description: "How to build SQL from Python expressions: Engine, Table, the insert/select/update/delete helpers, and one codebase for different DBMSs." +--- + # SQLAlchemy Core: SQL from Python expressions -In the previous article we ran SQL queries through `sqlite3`. That works, but two problems linger. +In the previous chapter, SQL queries lived in strings, and as long as the query is constant, a string works fine. But more often the query depends on data: say, finding a task by a title the user typed. The first thing that comes to mind is an f-string: + +```python +query = f"SELECT * FROM tasks WHERE title = '{name}'" +``` + +It works right up to the day `name` contains an apostrophe or someone else's chunk of SQL. That convenience hides two problems with raw strings. -**First: SQL lives in a string**, and any typo or careless interpolation of user data is a potential SQL injection. The `?` parameter saves you, but you have to remember it every single time. +**First: SQL lives in a string**, and any careless interpolation of user data is a potential SQL injection. The `?` parameter saves you, but you have to remember it every single time. **Second: every DBMS has its own SQL dialect.** If the app was written against SQLite and later moves to PostgreSQL, you'll almost certainly have to rewrite parts of the queries. -**SQLAlchemy Core** solves both: SQL is built from Python expressions, safety is on by default, and the same code runs against PostgreSQL, MySQL, SQLite. Plain `SELECT/WHERE` looks similar across the three, but as soon as you reach vendor-specific functions (dates, strings, aggregates) or schema, the syntax diverges, and Core translates your Python into the right dialect: +**SQLAlchemy Core** solves both: SQL is built from Python expressions, safety is on by default, and the same code runs against PostgreSQL, MySQL, SQLite. Plain `SELECT/WHERE` looks similar across the three, but as soon as you reach vendor-specific functions (dates, strings, aggregates), the syntax diverges, and Core translates your Python into the right dialect: + +One expression β€” three dialects: Postgres and MySQL match, SQLite gets its own variant, and each has its own placeholder + +There are really only four new words in Core: + +- `Engine` β€” the connection to the database; +- `Table` β€” the table definition; +- `insert`/`select`/`update`/`delete` β€” helpers instead of SQL strings; +- `.c` β€” column access. + +Everything else is the SQL you know from the previous chapter. ## Install @@ -18,32 +41,34 @@ For SQLite no extra drivers are needed. For PostgreSQL install `psycopg2-binary` ## Engine: the connection -`Engine` is the object responsible for talking to the database. Created once per application: +`Engine` is the object responsible for talking to the database. Created once per application, with the database address given by a connection string: -```python-executable +```python from sqlalchemy import create_engine -engine = create_engine('sqlite:///tasks.db', echo=True) +engine = create_engine('sqlite:///core_tasks.db') print("Engine ready") -# Output: Engine ready + +Engine ready + ``` -`echo=True` logs the SQL statements being executed. Handy while learning and debugging; turn it off in production. +`create_engine` has an `echo=True` parameter: with it, every executed SQL statement is printed to the console. Handy while debugging; turn it off in production. Connection strings for other DBMS: -- `postgresql://user:pass@host:5432/dbname` -- `mysql+pymysql://user:pass@host/dbname` -- `sqlite:///file.db` +- `postgresql://user:pass@host:5432/dbname` +- `mysql+pymysql://user:pass@host/dbname` +- `sqlite:///file.db` ## Defining a table In Core, table structure is described with a `Table` object β€” the Python equivalent of `CREATE TABLE`: -```python-executable +```python from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean -engine = create_engine('sqlite:///tasks.db') +engine = create_engine('sqlite:///core_tasks.db') metadata = MetaData() tasks_table = Table( @@ -58,105 +83,127 @@ tasks_table = Table( metadata.create_all(engine) print("Table tasks ready") -# Output: Table tasks ready + +Table tasks ready + ``` -`MetaData` is the collection of all `Table` objects in the app. `metadata.create_all(engine)` creates every table in the collection that isn't already in the DB. +`nullable=False` is the `NOT NULL` from the previous chapter: the field is required. `MetaData` is the collection of all `Table` objects in the app. `metadata.create_all(engine)` creates every table in the collection that isn't already in the DB. + +## The same four operations, as expressions + +The same CREATE, READ, UPDATE, DELETE as in the previous chapter β€” only Python expressions instead of SQL strings. Core ships a helper for each operation: `insert()`, `select()`, `update()`, `delete()`. -## CRUD: four operations +What's new in the familiar four: a batch insert from a list, column access via `.c`, and Python operators inside `.where()`. The database in the examples is created in memory: the `sqlite:///:memory:` connection string gives a clean database on every run. + +### INSERT and SELECT + +```python +from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean +from sqlalchemy import insert, select -For each operation Core ships a helper: `insert()`, `select()`, `update()`, `delete()`. Going forward we assume `engine` and `tasks_table` are defined as above. +# Setup: the engine and table from the examples above +engine = create_engine('sqlite:///:memory:') +metadata = MetaData() -### INSERT +tasks_table = Table( + 'tasks', metadata, + Column('id', Integer, primary_key=True), + Column('title', String, nullable=False), + Column('completed', Boolean, default=False), +) -```python-executable -from sqlalchemy import insert +metadata.create_all(engine) with engine.connect() as connection: result = connection.execute( insert(tasks_table), [ - {'title': 'Learn SQLAlchemy Core', 'completed': False}, - {'title': 'Write an app', 'completed': False}, - {'title': 'Test the code', 'completed': False}, + {'title': 'Learn SQLAlchemy Core'}, + {'title': 'Write an application'}, ], ) connection.commit() + print(f"Rows added: {result.rowcount}") + +Rows added: 2 + -print(f"Rows inserted: {result.rowcount}") -# Output: Rows inserted: 3 -``` - -Values are passed as a list of dicts β€” that's a batch insert in one query. SQLAlchemy substitutes parameters safely. - -### SELECT - -```python-executable -from sqlalchemy import select - -with engine.connect() as connection: result = connection.execute(select(tasks_table)) for row in result: print(row.id, row.title, row.completed) -# Output: -# 1 Learn SQLAlchemy Core 0 -# 2 Write an app 0 -# 3 Test the code 0 + +1 Learn SQLAlchemy Core False +2 Write an application False + ``` -Access columns by name (`row.title`), not by index like in `sqlite3`. Filter conditions go through `.where()`: +The values go into `insert` as a list of dictionaries: a batch insert in a single query, with SQLAlchemy substituting the parameters safely. -```python-executable -from sqlalchemy import select +We didn't set the `completed` column, because the `default=False` from the table definition kicked in. -with engine.connect() as connection: - result = connection.execute( - select(tasks_table).where(tasks_table.c.id == 1) - ) - row = result.first() - print(row.title) -# Output: Learn SQLAlchemy Core -``` +Rows are read by name (`row.title`), not by index as in `sqlite3`. And `completed` came back as `False`, not `0`: SQLAlchemy knows the column type (`Boolean`) and converts the value to the Python type itself. -`tasks_table.c.id` reads as "the `id` column of the `tasks` table". Comparisons (`==`, `>`, `<`, `.in_()`, `.like()`) become SQL automatically. +### UPDATE and DELETE -### UPDATE +A filter condition is added with the `.where()` method, rows are changed by the `update()` helper and removed by `delete()`: + +```python +from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean +from sqlalchemy import insert, select, update, delete -```python-executable -from sqlalchemy import update +# Setup: engine, table and two tasks +engine = create_engine('sqlite:///:memory:') +metadata = MetaData() + +tasks_table = Table( + 'tasks', metadata, + Column('id', Integer, primary_key=True), + Column('title', String, nullable=False), + Column('completed', Boolean, default=False), +) + +metadata.create_all(engine) with engine.connect() as connection: - result = connection.execute( + connection.execute(insert(tasks_table), [ + {'title': 'Learn SQLAlchemy Core'}, + {'title': 'Write an application'}, + ]) + connection.commit() + + connection.execute( update(tasks_table) .where(tasks_table.c.id == 1) .values(completed=True) ) + connection.execute( + delete(tasks_table).where(tasks_table.c.id == 2) + ) connection.commit() -print(f"Rows updated: {result.rowcount}") -# Output: Rows updated: 1 + result = connection.execute(select(tasks_table)) + for row in result: + print(row.id, row.title, row.completed) + +1 Learn SQLAlchemy Core True + ``` -### DELETE +`tasks_table.c.id` is "the `id` column of the `tasks` table", and `==` turns into a SQL comparison automatically; `>`, `<`, `.in_()` and `.like()` work the same way. -```python-executable -from sqlalchemy import delete +The final `select` confirms it: the first task is the only one left, already with `completed=True`. Both operations worked. -with engine.connect() as connection: - result = connection.execute( - delete(tasks_table).where(tasks_table.c.id == 3) - ) - connection.commit() +## Understanding check -print(f"Rows deleted: {result.rowcount}") -# Output: Rows deleted: 1 -``` +**The main advantage of SQLAlchemy Core over raw SQL strings in `sqlite3`?** -## What's next? +1. Queries run faster β€” Performance is roughly equivalent β€” sometimes Core is even slightly slower due to building SQL from expressions. The main advantage is not speed. -The next article covers **SQLAlchemy ORM** β€” a layer above Core where tables become Python classes and rows become objects, and you barely think in SQL terms. Great for typical business logic; Core stays in the toolbox for cases where you need precise query control. +2. **Correct answer:** SQL injection protection and portability across DBMS β€” The value goes to the driver separately from the query text, so you never interpolate it into a string by hand, and the same Python code works against PostgreSQL, MySQL, and SQLite β€” Core adapts the SQL to the chosen RDBMS. ---- +3. You no longer need to write SQL at all β€” Knowing SQL is still required: Core maps directly onto SQL operations select, insert, update, delete. It's just Python syntax instead of strings. -**The main advantage of SQLAlchemy Core over raw SQL strings in `sqlite3`?** +4. Automatic schema documentation generation β€” Core does not generate docs. The real wins are safety and portability across databases. +The next article covers **SQLAlchemy ORM** β€” a layer above Core where tables become Python classes and rows become objects, and you barely think in SQL terms. Great for typical business logic; Core stays in the toolbox for cases where you need precise query control. diff --git a/course/module-3/sqlalchemy-orm-basics/article.md b/course/module-3/sqlalchemy-orm-basics/article.md index 5eb31de..2e47ffd 100644 --- a/course/module-3/sqlalchemy-orm-basics/article.md +++ b/course/module-3/sqlalchemy-orm-basics/article.md @@ -1,14 +1,26 @@ +--- +meta: + title: "SQLAlchemy ORM: Database Access through Python Objects" + description: "Models as classes, rows as objects: DeclarativeBase and Mapped, Session, CRUD through attributes, and relationship links." +--- + # SQLAlchemy ORM: database access through Python objects -In Core we built SQL from Python expressions: `select(tasks).where(tasks.c.id == 1)`. That's a huge step up from raw SQL strings, but the code still talks in terms of "tables + columns" rather than familiar objects. +In Core we built SQL from Python expressions like `select(...).where(...)`. That's a huge step up from raw SQL strings, but the code still talks in terms of "tables + columns" rather than familiar objects. **ORM** (Object-Relational Mapping) takes one more step: a table is described as a Python class, a row of that table is an instance of that class, and changing an object's attribute is automatically reflected in the database. The result is working with the DB in the language of regular Python objects. +**Python object:** `Task(id=1, title="Learn SQLite")` + +**row in tasks:** `1 | Learn SQLite` + +The object's attributes match the row's columns, and the Session keeps both sides in sync + ## The model: a class as a table -In SQLAlchemy 2.0+ models are declared with `DeclarativeBase` and type annotations. This modern style replaces the older `declarative_base()`: +In SQLAlchemy 2.0+ models are declared with `DeclarativeBase` and type annotations: -```python-executable +```python from sqlalchemy import create_engine from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column @@ -25,48 +37,61 @@ class Task(Base): def __repr__(self): return f"Task(id={self.id}, title={self.title!r}, completed={self.completed})" -engine = create_engine('sqlite:///orm_tasks.db') +engine = create_engine('sqlite:///:memory:') Base.metadata.create_all(engine) -print("Model Task and table tasks ready") -# Output: Model Task and table tasks ready +print("Model Task and table tasks are ready") + +Model Task and table tasks are ready + ``` -How to read this: +How to read it: -- `class Task(Base)` β€” the model, a subclass of the base class -- `__tablename__ = 'tasks'` β€” the table name in the DB -- `id: Mapped[int] = mapped_column(primary_key=True)` β€” column `id`, type int, primary key -- `title: Mapped[str]` β€” column `title`, type str, NOT NULL by default -- `completed: Mapped[bool] = mapped_column(default=False)` β€” column `completed`, type bool, default `False` +- `class Task(Base)` β€” a model; `Base` is the shared parent of all models, through which SQLAlchemy collects the list of tables, like `MetaData` in Core; +- `__tablename__` β€” the table name in the DB; +- lines like `title: Mapped[str]` β€” columns: `Mapped[...]` marks "this is a table column", and the type in the brackets becomes the column type (`int` β†’ INTEGER, `str` β†’ VARCHAR, `bool` β†’ BOOLEAN); +- `mapped_column(...)` is added only when the column has settings β€” a primary key or a default value; +- `__repr__` β€” the special method you know: how the object presents itself when printed. -Python types (`int`, `str`, `bool`) map automatically to SQL types (`INTEGER`, `VARCHAR`, `BOOLEAN`). No separate `Column(Integer, ...)` calls like in Core. +That's why `Base.metadata.create_all(engine)` looks familiar: it's the same `create_all` as in the previous chapter. -## Session: unit of work +## Session: the unit of work -The ORM runs queries through a `Session`. A session is a "unit of work": it holds loaded objects in memory, tracks changes, and flushes everything to the DB in one call. +Queries in the ORM go through a `Session` β€” the "unit of work": it keeps loaded objects in memory, tracks changes, and saves everything to the DB with a single command. -```python-executable -from sqlalchemy.orm import Session +You open it with `with Session(engine) as session:` β€” that way the session closes itself, and `commit()` saves the accumulated changes. -with Session(engine) as session: - # ... work with objects here - session.commit() - -print("Session closed") -# Output: Session closed -``` +## CRUD through objects -`with Session(...)` closes the session on exit. `commit()` saves accumulated changes. +The same four operations for the third time β€” but now you write neither SQL nor expressions: you change Python objects, and the Session turns that into the right queries. -## CRUD through objects +CREATE, READ and DELETE are recognizable at a glance; the real news in the ORM is change tracking and links between tables. -Going forward we assume `engine` and the `Task` class are already defined. +The database in the examples is created in memory: the `sqlite:///:memory:` connection string gives a clean database on every run. ### CREATE -```python-executable -from sqlalchemy.orm import Session +```python +from sqlalchemy import create_engine +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session + +# Setup: the model and table from the beginning of the article +class Base(DeclarativeBase): + pass + +class Task(Base): + __tablename__ = 'tasks' + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] + completed: Mapped[bool] = mapped_column(default=False) + + def __repr__(self): + return f"Task(id={self.id}, title={self.title!r}, completed={self.completed})" + +engine = create_engine('sqlite:///:memory:') +Base.metadata.create_all(engine) with Session(engine) as session: task1 = Task(title="Learn ORM") @@ -75,99 +100,143 @@ with Session(engine) as session: session.commit() print(task1) print(task2) -# Output: -# Task(id=1, title='Learn ORM', completed=False) -# Task(id=2, title='Write code', completed=False) + +Task(id=1, title='Learn ORM', completed=False) +Task(id=2, title='Write code', completed=False) + ``` -Notice: `task1.id` is already populated after `commit()`. The DB assigned it automatically. +Notice: `task1.id` is already filled in after `commit()`. The database assigned it automatically. ### READ -In modern SQLAlchemy 2.0 queries are written via `select()` + `session.execute()`. The old `session.query(...)` still works but is considered legacy. +Queries are written with the `select()` you know from Core, and `session.execute()` runs them: + +```python +from sqlalchemy import create_engine, select +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session + +# Setup: the model, the table and three tasks (one already done) +class Base(DeclarativeBase): + pass + +class Task(Base): + __tablename__ = 'tasks' -```python-executable -from sqlalchemy import select -from sqlalchemy.orm import Session + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] + completed: Mapped[bool] = mapped_column(default=False) + + def __repr__(self): + return f"Task(id={self.id}, title={self.title!r}, completed={self.completed})" + +engine = create_engine('sqlite:///:memory:') +Base.metadata.create_all(engine) + +with Session(engine) as session: + session.add_all([ + Task(title="Learn ORM"), + Task(title="Write code"), + Task(title="Ship the project", completed=True), + ]) + session.commit() with Session(engine) as session: # All rows - tasks = session.execute(select(Task)).scalars().all() + stmt = select(Task) + tasks = session.execute(stmt).scalars().all() for task in tasks: print(task) -# Output: -# Task(id=1, title='Learn ORM', completed=False) -# Task(id=2, title='Write code', completed=False) + +Task(id=1, title='Learn ORM', completed=False) +Task(id=2, title='Write code', completed=False) +Task(id=3, title='Ship the project', completed=True) + + + # One record by primary key + task = session.get(Task, 1) + print(task) + +Task(id=1, title='Learn ORM', completed=False) + + + # With a filter: pending only + pending = session.execute(select(Task).where(Task.completed == False)).scalars().all() + for task in pending: + print(task) + +Task(id=1, title='Learn ORM', completed=False) +Task(id=2, title='Write code', completed=False) + ``` -`.scalars()` is needed because `select(Task)` returns row-tuples (even if each tuple has one element). `.scalars()` unwraps them into `Task` instances. +`.scalars()` is needed because `select(Task)` returns row tuples: it unpacks them into `Task` objects. -Fetching a single record by primary key is simpler via `session.get`: +A single record by primary key is fastest through `session.get`. -```python-executable -from sqlalchemy.orm import Session +The filter reads like in Core, except instead of `tasks_table.c.completed` it's the class attribute `Task.completed`: the third task is done, so it didn't make the selection. -with Session(engine) as session: - task = session.get(Task, 1) - print(task) -# Output: Task(id=1, title='Learn ORM', completed=False) -``` +### UPDATE and DELETE -With a filter: +The most convenient part of the ORM: change an object's attribute, and the Session figures out what to update. Deletion goes through `session.delete`: -```python-executable -from sqlalchemy import select -from sqlalchemy.orm import Session +```python +from sqlalchemy import create_engine, select +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session -with Session(engine) as session: - stmt = select(Task).where(Task.completed == False) - pending = session.execute(stmt).scalars().all() - for task in pending: - print(task) -# Output: -# Task(id=1, title='Learn ORM', completed=False) -# Task(id=2, title='Write code', completed=False) -``` +# Setup: the model, the table and two tasks +class Base(DeclarativeBase): + pass + +class Task(Base): + __tablename__ = 'tasks' + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] + completed: Mapped[bool] = mapped_column(default=False) -### UPDATE + def __repr__(self): + return f"Task(id={self.id}, title={self.title!r}, completed={self.completed})" -The most comfortable part of ORM: change an attribute on an object and the Session figures out what to update. +engine = create_engine('sqlite:///:memory:') +Base.metadata.create_all(engine) -```python-executable -from sqlalchemy.orm import Session +with Session(engine) as session: + session.add_all([Task(title="Learn ORM"), Task(title="Write code")]) + session.commit() with Session(engine) as session: task = session.get(Task, 1) task.completed = True session.commit() - print(task) -# Output: Task(id=1, title='Learn ORM', completed=True) + + task2 = session.get(Task, 2) + session.delete(task2) + session.commit() + + tasks = session.execute(select(Task)).scalars().all() + for task in tasks: + print(task) + +Task(id=1, title='Learn ORM', completed=True) + ``` -No explicit `UPDATE ... SET ... WHERE ...`. The Session tracks changed attributes and emits the right SQL on `commit()`. +No explicit `UPDATE ... SET` or `DELETE ... WHERE`: the Session tracks changed attributes and deleted objects and sends the right SQL on `commit()`. One task remains, and it's already done. -### DELETE +## Links between tables -```python-executable -from sqlalchemy.orm import Session +Real schemas link tables together: a user has tasks, a post has comments. -with Session(engine) as session: - task = session.get(Task, 2) - session.delete(task) - session.commit() - print("Task deleted") -# Output: Task deleted -``` +The link rests on a **foreign key**: the `user_tasks` table has a `user_id` column holding the `id` of the owning user β€” that's how a task row knows whose it is. `ForeignKey("users.id")` tells the database that the value in this column must exist in `users.id`, otherwise the task would belong to no one. -## Relationships between tables +`relationship` is the ORM's layer on top of that column: instead of manually finding all rows with the right `user_id`, you write `user.tasks`. -Real schemas are connected: a user has tasks, a post has comments. The ORM describes relationships with `relationship`, and accessing related records reads like accessing a regular attribute: +`back_populates` ties the two sides together so that `user.tasks` and `task.user` describe one link, not two independent ones. In code, reaching related records looks like accessing a regular attribute: -```python-executable -from sqlalchemy import ForeignKey +```python +from sqlalchemy import create_engine, ForeignKey from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session -from sqlalchemy import create_engine -from typing import List class Base(DeclarativeBase): pass @@ -176,7 +245,7 @@ class User(Base): __tablename__ = 'users' id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] - tasks: Mapped[List["UserTask"]] = relationship(back_populates="user") + tasks: Mapped[list["UserTask"]] = relationship(back_populates="user") class UserTask(Base): __tablename__ = 'user_tasks' @@ -185,7 +254,7 @@ class UserTask(Base): user_id: Mapped[int] = mapped_column(ForeignKey("users.id")) user: Mapped["User"] = relationship(back_populates="tasks") -engine = create_engine('sqlite:///orm_users.db') +engine = create_engine('sqlite:///:memory:') Base.metadata.create_all(engine) with Session(engine) as session: @@ -200,32 +269,44 @@ with Session(engine) as session: print(user.name) for task in user.tasks: print(f" {task.title}") -# Output: -# Anna -# Learn Python -# Write code + +Anna + Learn Python + Write code + ``` -Behind the scenes, `user.tasks` runs `SELECT ... FROM user_tasks WHERE user_id = ?`, but in code it looks like a plain attribute access. That's the central comfort of the ORM: a relational link reads as "a user has tasks". +The name `"UserTask"` in the annotation is quoted because that class is declared further down the file: at the moment `User` is being read, Python doesn't know it yet. + +Behind the scenes `user.tasks` runs a SQL query that picks the rows of `user_tasks` with the right `user_id`. But in code it looks like plain attribute access. That's the main comfort of the ORM: a relational link reads as "the user has tasks". ## Comparing the three approaches -| Aspect | sqlite3 | SQLAlchemy Core | SQLAlchemy ORM | -| --------------------- | ---------------------- | ---------------------- | -------------------- | -| Query | SQL string | Python expression | Python object | -| Injection protection | manual via `?` | automatic | automatic | -| Cross-DB portability | no | yes | yes | -| Relationships | hand-written JOINs | JOIN expressions | `user.tasks` | -| UPDATE | `UPDATE ... SET ...` | `update().values(...)` | `obj.field = ...` | -| SQL control | maximum | high | medium | +| Aspect | sqlite3 | SQLAlchemy Core | SQLAlchemy ORM | +| -------------------- | -------------------- | ---------------------- | ----------------- | +| Query | SQL string | Python expression | Python object | +| Injection protection | manual, via `?` | automatic | automatic | +| Cross-DB portability | no | yes | yes | +| Links | manual JOINs | JOIN expressions | `user.tasks` | +| UPDATE | `UPDATE ... SET ...` | `update().values(...)` | `obj.field = ...` | +| Control over SQL | maximum | high | medium | -A good rule: ORM for typical business logic, Core for complex queries where you need control, raw SQL only when neither of the first two will do. +A good rule: -## What's next? +- ORM β€” for typical business logic; +- Core β€” for complex queries where you need control; +- raw SQL β€” only when the first two fall short. -The ORM is a tool that optimizes the **typical** cases of DB work. If 95% of your queries are "load an object, change a field, save", the ORM saves a lot of time. When you hit a complex query or a performance-critical path, drop down into Core or write SQL directly. The three layers complement each other. +## Understanding check ---- +**The main advantage of the ORM over Core?** + +1. Queries run faster β€” The opposite: the ORM adds a small overhead compared to Core (object tracking). The advantage is convenience, not speed. + +2. **Correct answer:** Working with regular Python objects instead of SQL expressions, with automatic change tracking β€” Change an attribute β€” the Session issues the right UPDATE on commit(). Links between tables read as attributes (user.tasks), not as explicit JOINs. + +3. The ability to work without understanding SQL β€” You still need to understand SQL: the ORM runs SQL under the hood, and without knowing what it does you easily end up with extra slow queries. The advantage of the ORM is convenience, not "magic freedom from SQL". -**The main advantage of ORM over Core?** +4. The ORM validates the DB schema at runtime β€” That is more a feature of typed models (Mapped\[str]), and the checking happens in your IDE or linter, not at runtime. The main advantage of the ORM is working with objects instead of SQL. +The ORM is a tool that optimizes the **typical** cases of database work. If 95% of your queries are "get an object, change a field, save", the ORM saves a lot of time. When you hit a complex query or a performance bottleneck, drop down to Core or write SQL directly. The three levels complement each other. diff --git a/course/module-3/sqlite-python-basics/article.md b/course/module-3/sqlite-python-basics/article.md index 7bcc09f..a6defe8 100644 --- a/course/module-3/sqlite-python-basics/article.md +++ b/course/module-3/sqlite-python-basics/article.md @@ -1,16 +1,24 @@ -# SQLite in Python: relational database basics +--- +meta: + title: "SQLite: Your First Database in Python" + description: "Connecting to SQLite from Python, creating a table, the four CRUD operations, and SQL injection protection with parameters." +--- -Most production apps store data in **relational databases** β€” PostgreSQL, MySQL, SQLite β€” not in files. Sooner or later you'll write SQL queries from Python and read results back into your code. +# SQLite: Your First Database in Python -In this article we'll walk through the basic pattern using **SQLite**: a full-featured relational database built into Python. No installation needed, the database lives in a single file, and the same approach carries over to any other RDBMS. +Your phone's messages, your browser's history, the settings of half the apps on your laptop β€” a huge share of them live in **SQLite**: a serverless relational database built right into Python. Nothing to install, the whole database is a single file, and the same approach later carries over to any other RDBMS. -This article gives a **general overview** of working with SQL from Python. For a deep dive into SQL itself, see the [free SQL Academy course](https://sql-academy.org/en/guide) with practical exercises. +SQLite is where we'll learn the basic pattern for working with a relational database: how to send a SQL query from Python and read the rows back into code. ## Connection and cursor -Python's standard library includes the `sqlite3` module. Basic pattern: open a connection, get a `cursor` (which runs queries), close the connection. +Python's standard library includes the `sqlite3` module. The basic pattern is three steps: + +- open a **connection** β€” an opened database, like a file after `open()`; +- get a **cursor** β€” it sends queries into the database and holds the latest result; +- close the connection at the end. -```python-executable +```python import sqlite3 # Connect to the database (the file is created automatically) @@ -21,61 +29,60 @@ cursor = connection.cursor() connection.close() print("Done") -# Output: Done + +Done + ``` -It's nicer to use the connection as a context manager (`with`) β€” it closes itself and commits changes on exit: - -```python-executable -import sqlite3 - -with sqlite3.connect('tasks.db') as connection: - cursor = connection.cursor() - # ... queries - -print("Done") -# Output: Done -``` +`Python code` β†’ SQL query β†’ `tasks.db` β†’ rows as tuples β†’ `Python code` ## Creating a table -In a relational database, data lives in **tables**. Each table has a schema: which columns, what types, what constraints. You create one with the SQL `CREATE TABLE` command: +In a relational database, data lives in **tables**. Every table is described by a schema: which columns, of which types, with which constraints. We create one with the SQL command `CREATE TABLE`. + +From here on, every example wraps the connection in `with`: SQLite doesn't write changes instantly: they have to be confirmed (committed), and `with` does that itself on leaving the block, rolling back on error. It doesn't close the connection, though, but in short scripts like the ones below it closes together with the program; in long-running code call `close()`, as in the first example. -```python-executable +```python import sqlite3 with sqlite3.connect('tasks.db') as connection: cursor = connection.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS tasks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id INTEGER PRIMARY KEY, title TEXT NOT NULL, completed BOOLEAN DEFAULT FALSE ) ''') -print("Table tasks ready") -# Output: Table tasks ready +print("Table tasks is ready") + +Table tasks is ready + ``` What the SQL parts mean: -- `CREATE TABLE IF NOT EXISTS tasks` β€” create table `tasks` if it doesn't exist yet -- `id INTEGER PRIMARY KEY AUTOINCREMENT` β€” integer primary key, auto-incremented -- `title TEXT NOT NULL` β€” text field, required -- `completed BOOLEAN DEFAULT FALSE` β€” boolean field, defaults to `False` +- `CREATE TABLE IF NOT EXISTS tasks` β€” create the `tasks` table if it doesn't exist yet +- `id INTEGER PRIMARY KEY` β€” an integer primary key; SQLite numbers new rows itself +- `title TEXT NOT NULL` β€” a text field, required +- `completed BOOLEAN DEFAULT FALSE` β€” a boolean field, `False` by default ## CRUD: the four basic operations -CRUD stands for **C**reate / **R**ead / **U**pdate / **D**elete: the four operations that cover almost all data work. Going forward we'll assume `tasks` table is already created. +CRUD is an acronym for **C**reate / **R**ead / **U**pdate / **D**elete: four operations that cover almost all work with data. + +We practice on an in-memory database: `:memory:` instead of a file name gives a clean database on every run. The first lines inside `with` are the setup: a table and two tasks. -### CREATE: inserting data +### CREATE: adding data -```python-executable +```python import sqlite3 -with sqlite3.connect('tasks.db') as connection: +with sqlite3.connect(':memory:') as connection: cursor = connection.cursor() + cursor.execute("CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT)") + cursor.execute( "INSERT INTO tasks (title) VALUES (?)", ("Learn SQLite",) @@ -86,105 +93,121 @@ with sqlite3.connect('tasks.db') as connection: ) print("Tasks added") -# Output: Tasks added + +Tasks added + ``` -A crucial point: values are **never embedded directly in the SQL string** via f-strings or concatenation. Instead, use the `?` placeholder and pass the value as the second argument to `execute`. This guards against **SQL injection**: +The values travel as a tuple in the second argument of `execute`, and the SQL itself holds a `?` parameter in their place. The comma in `("Learn SQLite",)` is required: it is what makes the parentheses a one-element tuple. Why values are never glued straight into the query string β€” we'll show right after we learn to read data. -```python -# UNSAFE: user input concatenated into SQL -user_input = "'; DROP TABLE tasks; --" -cursor.execute(f"INSERT INTO tasks (title) VALUES ('{user_input}')") -# becomes: INSERT ... VALUES (''); DROP TABLE tasks; --') - -# SAFE: value passed separately -cursor.execute("INSERT INTO tasks (title) VALUES (?)", (user_input,)) -# becomes: INSERT ... VALUES ('\'; DROP TABLE tasks; --') -``` +### READ: reading data -Rule: **never concatenate user input into a SQL string**, always pass it through `?` parameters. - -### READ: querying data - -```python-executable +```python import sqlite3 -with sqlite3.connect('tasks.db') as connection: +with sqlite3.connect(':memory:') as connection: cursor = connection.cursor() - cursor.execute("SELECT id, title, completed FROM tasks") + cursor.execute("CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT)") + cursor.execute("INSERT INTO tasks (title) VALUES ('Learn SQLite'), ('Buy groceries')") + + cursor.execute("SELECT id, title FROM tasks") rows = cursor.fetchall() for row in rows: print(row) -# Output: -# (1, 'Learn SQLite', 0) -# (2, 'Buy groceries', 0) + +(1, 'Learn SQLite') +(2, 'Buy groceries') + ``` -`cursor.fetchall()` returns all rows as a list of tuples. Access fields by index: `row[0]` is `id`, `row[1]` is `title`, etc. +`cursor.fetchall()` returns all result rows as a list of tuples: fields are accessed by index, `row[0]` is `id`, `row[1]` is `title`. -For a single row (say by id), use `fetchone()`: +When you need just one row, use `fetchone()` instead: it returns the first row of the result. We'll see it in action just below. -```python-executable -import sqlite3 +### SQL injection: why `?` -with sqlite3.connect('tasks.db') as connection: - cursor = connection.cursor() - cursor.execute("SELECT title FROM tasks WHERE id = ?", (1,)) - row = cursor.fetchone() +Now we can show what the `?` parameter is really for. Imagine a search field where the user types a task title β€” and an attacker types a piece of SQL: -print(row) -# Output: ('Learn SQLite',) +```python +# DANGEROUS: user input is glued into the SQL +search = "' OR '1'='1" +cursor.execute(f"SELECT * FROM tasks WHERE title = '{search}'") +# The SQL becomes: SELECT * FROM tasks WHERE title = '' OR '1'='1' +# '1'='1' is always true β†’ ALL tasks come back, not just the right one + +# SAFE: the value travels separately +cursor.execute("SELECT * FROM tasks WHERE title = ?", (search,)) +# looks for a task literally titled "' OR '1'='1" β€” nothing extra comes back ``` +The asterisk in `SELECT *` means "all columns at once". The attacker put a piece of SQL into an ordinary search field and got every row of the table; the same trick bypasses password checks or deletes data. The rule: **never glue user input into a SQL string**, always pass parameters through `?`. + ### UPDATE: changing data -```python-executable +```python import sqlite3 -with sqlite3.connect('tasks.db') as connection: +with sqlite3.connect(':memory:') as connection: cursor = connection.cursor() + cursor.execute("CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT, completed BOOLEAN DEFAULT FALSE)") + cursor.execute("INSERT INTO tasks (title) VALUES ('Learn SQLite'), ('Buy groceries')") + cursor.execute( "UPDATE tasks SET completed = ? WHERE id = ?", (True, 1) ) -print("Task 1 marked completed") -# Output: Task 1 marked completed + cursor.execute("SELECT id, title, completed FROM tasks WHERE id = ?", (1,)) + print(cursor.fetchone()) + +(1, 'Learn SQLite', 1) + ``` -`WHERE id = ?` is essential: without a condition, `UPDATE` updates **every** row in the table. +The first task is now done: SQLite stores boolean values as `0` and `1`, and `completed` flipped from zero to one. And `WHERE id = ?` is essential: without a condition, `UPDATE` changes **every** row in the table. ### DELETE: removing data -```python-executable +```python import sqlite3 -with sqlite3.connect('tasks.db') as connection: +with sqlite3.connect(':memory:') as connection: cursor = connection.cursor() + cursor.execute("CREATE TABLE tasks (id INTEGER PRIMARY KEY, title TEXT)") + cursor.execute("INSERT INTO tasks (title) VALUES ('Learn SQLite'), ('Buy groceries')") + cursor.execute("DELETE FROM tasks WHERE id = ?", (2,)) -print("Task 2 deleted") -# Output: Task 2 deleted + cursor.execute("SELECT id, title FROM tasks") + print(cursor.fetchall()) + +[(1, 'Learn SQLite')] + ``` -Same warning: without a `WHERE`, `DELETE` empties **the whole table**. +The second task is gone β€” one row remains. Same story as with `UPDATE`: without `WHERE`, the `DELETE` command removes **every** row in the table. -## What's beyond this article +## What we left out -There are several production topics we're not covering in depth here but you should know they exist: +Real production code involves a few important topics we don't cover in detail here, but you should know they exist: -- **Transactions** (`BEGIN`/`COMMIT`/`ROLLBACK`): a group of changes runs atomically, all or none. The `with sqlite3.connect(...)` block commits automatically on exit. -- **Relationships** (`FOREIGN KEY`, `JOIN`): most real schemas have multiple linked tables (users and their tasks, orders and items). -- **Indexes**: speed up lookups on frequently queried columns. +- **Transactions** (`BEGIN`/`COMMIT`/`ROLLBACK`): a group of changes runs atomically β€” all or nothing. `with sqlite3.connect(...)` commits automatically on leaving the block. +- **JOINs and multi-table queries**: most real schemas have several linked tables (users and their tasks, orders and products), and the data is pulled from them with a single query. +- **Indexes**: speed up searches on frequently used columns. -These are covered in the [SQL Academy course](https://sql-academy.org/en/guide). +These topics are covered by the [SQL Academy course](https://sql-academy.org/en/guide). -## What's next? +## Understanding check -The next article covers **SQLAlchemy Core**: a library that builds SQL queries from Python expressions instead of strings. SQL injections are handled automatically, and the same code works across PostgreSQL, MySQL, and SQLite. +**Why should values go into `execute()` through the `?` parameter rather than directly into the SQL string?** ---- +1. **Correct answer:** To protect against SQL injection β€” The query text and the values travel to the database through different channels: the query is parsed first, and the placeholder is a slot for a value. Whatever lands in the slot can no longer be read as SQL, so hostile input stays a plain string in a field, not a command. + +2. To make the query run faster β€” There is a speed effect (the DB may cache the query plan), but it is a side benefit. The main reason is safety. + +3. So SQLite can convert the types automatically β€” Type conversion happens either way. The main reason for parameterized queries is protection against SQL injection. -**Why pass values to `execute()` through the `?` parameter instead of embedding them in the SQL string?** +4. It is required by the SQL standard β€” The Β«?Β» placeholder is a feature of the sqlite3 driver, not of the SQL standard. Other DBMSs use other placeholders (%s, :name). The main reason to use them is safety. +In the next article we take on **SQLAlchemy Core**: a library that builds SQL queries from Python expressions instead of strings. SQL injection is prevented automatically, and the same code runs on PostgreSQL, MySQL and SQLite. diff --git a/course/module-3/testing/article.md b/course/module-3/testing/article.md index 5deebda..7ecaad9 100644 --- a/course/module-3/testing/article.md +++ b/course/module-3/testing/article.md @@ -1,3 +1,9 @@ +--- +meta: + title: "Testing in Python" + description: "Introduction to testing in Python: why tests are important, types of tests, and testing frameworks." +--- + # Testing in Python Imagine building a bridge πŸŒ‰ - would you want people to cross it without first checking if it's safe? The same principle applies to your code! Testing helps ensure your programs work as expected, even when you make changes or add new features. @@ -8,11 +14,11 @@ Imagine building a bridge πŸŒ‰ - would you want people to cross it without first Testing helps: -- Verify your code works correctly -- Find bugs before your users do -- Make changes with confidence -- Document how your code should behave -- Improve the design of your code +- Verify your code works correctly +- Find bugs before your users do +- Make changes with confidence +- Document how your code should behave +- Improve the design of your code ## Types of tests in Python @@ -20,7 +26,7 @@ In Python, there are several levels of testing, each with its own purpose: ### Unit tests -```python +```python-interactive # Example of a simple function and its unit test def add(a, b): return a + b @@ -30,6 +36,10 @@ result = add(2, 3) expected = 5 print(f"Expected: {expected}, Got: {result}") print(f"Test passed: {result == expected}") + +Expected: 5, Got: 5 +Test passed: True + ``` Unit tests check individual components (functions, methods, classes) in isolation. They're like testing each brick before building a wall. @@ -48,7 +58,7 @@ Python has several frameworks to make testing easier: ### Built-in: unittest -```python +```python-interactive import unittest class TestAddFunction(unittest.TestCase): @@ -66,6 +76,9 @@ test = TestAddFunction() test.test_add_positive_numbers() test.test_add_negative_numbers() print("All tests passed!") + +All tests passed! + ``` The `unittest` framework is included with Python and provides a rich set of tools for creating and running tests. @@ -85,7 +98,7 @@ def test_string_operation(): ### doctest -```python +```python-interactive def multiply(a, b): """ Multiply two numbers and return the result. @@ -100,6 +113,10 @@ def multiply(a, b): # In a real project, you'd use doctest.testmod() print(multiply(2, 3)) print(multiply(-1, 4)) + +6 +-4 + ``` `doctest` lets you include tests in your documentation, which helps keep your examples accurate. @@ -122,5 +139,12 @@ Let's check your understanding of testing basics: **Which of these is NOT a benefit of testing?** +1. Finding bugs early in development β€” This is actually one of the main benefits of testing. + +2. **Correct answer:** Making your code run faster β€” Testing verifies code correctness but doesn't inherently make it faster. + +3. Giving confidence when refactoring code β€” Tests help ensure that your changes don't break existing functionality. + +4. Documenting expected behavior β€” Tests serve as executable documentation of how your code should behave. In the next lessons, we'll explore each testing approach in more detail, starting with why tests are essential for any serious software project. diff --git a/course/module-3/threading-and-multiprocessing/article.md b/course/module-3/threading-and-multiprocessing/article.md index 7c0acf6..8a0fa0a 100644 --- a/course/module-3/threading-and-multiprocessing/article.md +++ b/course/module-3/threading-and-multiprocessing/article.md @@ -1,6 +1,16 @@ +--- +meta: + title: "Threads and processes in Python: threading and multiprocessing" + description: "The threading module for I/O-bound tasks and multiprocessing for CPU-bound. Creation, synchronization with Lock and Queue, choosing the right approach." +--- + # Threads and processes in Python -From the intro: I/O-bound tasks need **threads** (`threading`), CPU-bound tasks need **processes** (`multiprocessing`). This article covers both. The APIs are nearly identical, so once you learn one the other comes for free. +Eight heavy computations through a `ThreadPoolExecutor` take about twelve seconds. Change one word β€” `Thread` to `Process` β€” and the same eight finish in three or four. That gap is the whole difference between threads and processes. + +You remember the split from the intro: **threads** (`threading`) for I/O, **processes** (`multiprocessing`) for computation. This article covers both. The APIs are nearly identical, so once you learn one the other comes for free. + +![Illustration: left panel shows threading with one Process containing Thread 1-4 with shared memory and a GIL icon; right panel shows multiprocessing with three separate processes, each with its own Python and memory, with CPU cores below; caption: threads share memory and GIL, processes are isolated and truly parallel](https://python-academy.org/static/guidePage/threading-and-multiprocessing/threads-vs-processes-en.webp "Threads share memory inside one process (GIL); processes are isolated and can run on separate CPU cores") ## threading: threads inside one process @@ -25,14 +35,14 @@ t2.join() print("All threads done") ``` -- `target` β€” the function the thread runs -- `args` β€” a tuple of arguments -- `start()` β€” starts the thread -- `join()` β€” blocks the calling thread until this one finishes +- `target` β€” the function the thread runs +- `args` β€” a tuple of arguments +- `start()` β€” starts the thread +- `join()` β€” blocks the calling thread until this one finishes When you run this, you'll see something like: -``` +```text Thread A: sleeping for 2s Thread B: sleeping for 1s Thread B: done @@ -44,7 +54,28 @@ Thread B started second but finished first β€” its `sleep` is shorter, and `t1.j ## Protecting shared data: Lock -Threads share memory. If two threads modify the same variable, the **result is unpredictable** (race condition). The fix is a `Lock`: +Threads share memory. If two threads modify the same variable without protection, the **result is unpredictable** (race condition). Let's look: five threads each increment a counter a million times, so we should get 5,000,000. + +```python +import threading + +counter = 0 + +def increment(): + global counter + for _ in range(1_000_000): + counter += 1 + +threads = [threading.Thread(target=increment) for _ in range(5)] +for t in threads: + t.start() +for t in threads: + t.join() + +print(counter) # e.g. 3137095 β€” and a different number every run +``` + +We expected 5,000,000 but got less β€” and next time the number will differ. The reason: `counter += 1` isn't one action but three (read the value, add one, write it back), and threads sneak in between the steps, overwriting each other's increments. The fix is a `Lock`: while one thread is inside `with lock`, the others wait their turn. ```python import threading @@ -54,7 +85,7 @@ lock = threading.Lock() def increment(): global counter - for _ in range(100_000): + for _ in range(1_000_000): with lock: # automatic acquire/release counter += 1 @@ -64,12 +95,10 @@ for t in threads: for t in threads: t.join() -print(counter) # 500000 β€” correct +print(counter) # 5000000 β€” now always correct ``` -Without the `lock`, the total ends up at a random number below 500_000: threads "overwrite" each other's increments. The `with lock:` context manager is the standard usage and guarantees release even on exceptions. - -`threading` also has `Event`, `Semaphore`, `Condition`, `RLock`. In practice, `Lock` and `Queue` (next section) cover 90% of cases. The rest is for non-trivial coordination. +The `with lock:` context manager is the standard usage: it guarantees release even on exceptions. `threading` also has `Event`, `Semaphore`, `Condition`, `RLock`. In practice, `Lock` and `Queue` (next section) cover 90% of cases. The rest is for non-trivial coordination. ## Sharing data between threads: queue.Queue @@ -193,33 +222,40 @@ from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor def task(x): return x * x -# For I/O-bound: threads -with ThreadPoolExecutor(max_workers=4) as executor: - results = list(executor.map(task, range(10))) +# if __name__ is needed for ProcessPoolExecutor: it spawns processes +if __name__ == "__main__": + # For I/O-bound: threads + with ThreadPoolExecutor(max_workers=4) as executor: + results = list(executor.map(task, range(10))) -# For CPU-bound: processes -with ProcessPoolExecutor(max_workers=4) as executor: - results = list(executor.map(task, range(10))) + # For CPU-bound: processes β€” only the executor class changes + with ProcessPoolExecutor(max_workers=4) as executor: + results = list(executor.map(task, range(10))) ``` This is the most practical way to parallelize simple tasks. In modern code `concurrent.futures` shows up more often than raw `threading.Thread` or `multiprocessing.Process`. ## When to pick what -| Task | Tool | -| --- | --- | -| Simple I/O-bound in existing synchronous code | `ThreadPoolExecutor` | -| When you need manual state management (Lock, Queue) | `threading` directly | -| CPU-bound computation | `ProcessPoolExecutor` or `multiprocessing.Pool` | -| Thousands of network connections | **asyncio** (next articles) | +| Task | Tool | +| --------------------------------------------------- | ----------------------------------------------- | +| Simple I/O-bound in existing synchronous code | `ThreadPoolExecutor` | +| When you need manual state management (Lock, Queue) | `threading` directly | +| CPU-bound computation | `ProcessPoolExecutor` or `multiprocessing.Pool` | +| Thousands of network connections | **asyncio** (next articles) | For high-volume I/O, `asyncio` is better: single thread, minimal switching overhead. But it requires rewriting the code in `async` style. -## What's next? +## Understanding check -The next two articles cover **asyncio**: the third and most efficient way to organize concurrent I/O. It really shines for web servers, bots, and API clients. +**Why does `multiprocessing` work for CPU-bound tasks while `threading` does not?** ---- +1. Processes start faster than threads β€” The opposite: creating a process is more expensive than creating a thread. The advantage of multiprocessing is elsewhere. -**Why does `multiprocessing` work for CPU-bound tasks while `threading` does not?** +2. **Correct answer:** Each process has its own Python interpreter and its own GIL, so processes really run in parallel on multiple CPU cores β€” Correct. The GIL permits only one thread per process to execute Python bytecode at a time. Multiple processes run multiple interpreters, each with its own GIL and its own core β€” that is true parallelism. + +3. multiprocessing uses async features that threading lacks β€” Neither of them uses async. That is a separate model (asyncio). +4. threading cannot be used on multicore CPUs β€” threading works on any CPU. The limitation is that, due to the GIL, threads don't execute Python code simultaneously. For I/O-bound work that doesn't matter since the GIL is released while waiting. + +The next two articles cover **asyncio**: the third and most efficient way to organize concurrent I/O. It really shines for web servers, bots, and API clients. diff --git a/course/module-3/unittest-module-testing/article.md b/course/module-3/unittest-module-testing/article.md index 44fc635..1e27180 100644 --- a/course/module-3/unittest-module-testing/article.md +++ b/course/module-3/unittest-module-testing/article.md @@ -1,3 +1,9 @@ +--- +meta: + title: "unittest Module: Standard Testing Library" + description: "Master Python built-in unittest module, which provides a powerful framework for creating and running tests." +--- + # unittest Module: Standard Testing Library In previous articles, we explored testing with pytest and became familiar with coverage and continuous integration concepts. However, Python also offers a built-in testing solution β€” the **unittest** module. Understanding it is valuable both for grasping the fundamentals of testing in Python and for working with codebases that use this module. @@ -8,16 +14,16 @@ The `unittest` module is Python's standard library for testing, inspired by Java Key features of unittest: -- **Built-in**: Included in Python's standard library, requires no additional installation -- **OOP approach**: Tests are organized in classes that inherit from `unittest.TestCase` -- **Rich assertion set**: Provides numerous methods for verifying results -- **Setup and cleanup**: Supports methods for setting up and cleaning up the environment between tests +- **Built-in**: Included in Python's standard library, requires no additional installation +- **OOP approach**: Tests are organized in classes that inherit from `unittest.TestCase` +- **Rich assertion set**: Provides numerous methods for verifying results +- **Setup and cleanup**: Supports methods for setting up and cleaning up the environment between tests ## unittest Basics Let's create a simple test using unittest: -```python +```python-interactive import unittest def add(a, b): @@ -39,6 +45,13 @@ class TestAddFunction(unittest.TestCase): # Run the tests if __name__ == '__main__': unittest.main() + +... +---------------------------------------------------------------------- +Ran 3 tests in 0.001s + +OK + ``` As you can see, we: @@ -69,7 +82,7 @@ The `TestCase` class provides numerous assertion methods that help verify expect ### Example of Using Different Assertions -```python +```python-interactive import unittest class TestAssertionMethods(unittest.TestCase): @@ -100,6 +113,13 @@ class TestAssertionMethods(unittest.TestCase): if __name__ == '__main__': unittest.main() + +... +---------------------------------------------------------------------- +Ran 5 tests in 0.001s + +OK + ``` ## Test Environment Setup and Cleanup @@ -111,7 +131,7 @@ Unittest provides special methods for preparing the environment before tests run 3. **setUpClass()**: Runs once before all tests in the class 4. **tearDownClass()**: Runs once after all tests in the class -```python +```python-interactive import unittest class DatabaseTest(unittest.TestCase): @@ -143,13 +163,30 @@ class DatabaseTest(unittest.TestCase): if __name__ == '__main__': unittest.main() + +Connecting to database (executed once) + +Creating test data (before each test) +Running test 1 +Cleaning up test data (after each test) + +Creating test data (before each test) +Running test 2 +Cleaning up test data (after each test) +Closing database connection (executed once) +. +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +OK + ``` These methods are particularly useful when tests require common setup or cleanup of resources such as: -- Database connections -- Files and network connections -- Creating and deleting temporary data +- Database connections +- Files and network connections +- Creating and deleting temporary data ## Running Tests @@ -183,7 +220,7 @@ This command automatically finds all tests in the `tests` directory and runs the Unittest allows handling special cases in tests: -```python +```python-interactive import unittest import sys @@ -204,14 +241,21 @@ class TestSkippingAndFailing(unittest.TestCase): if __name__ == '__main__': unittest.main() + +... +---------------------------------------------------------------------- +Ran 3 tests in 0.001s + +OK (skipped=2, expected failures=1) + ``` Decorators for special cases: -- `@unittest.skip(reason)`: Always skips the test -- `@unittest.skipIf(condition, reason)`: Skips the test if the condition is true -- `@unittest.skipUnless(condition, reason)`: Skips the test if the condition is not true -- `@unittest.expectedFailure`: Marks the test as expected to fail +- `@unittest.skip(reason)`: Always skips the test +- `@unittest.skipIf(condition, reason)`: Skips the test if the condition is true +- `@unittest.skipUnless(condition, reason)`: Skips the test if the condition is not true +- `@unittest.expectedFailure`: Marks the test as expected to fail ## unittest vs pytest: When to Use Which? @@ -227,15 +271,15 @@ Decorators for special cases: **When to use unittest:** -- In projects where an OOP approach is preferred -- When you need to avoid external dependencies -- When working with existing unittest code +- In projects where an OOP approach is preferred +- When you need to avoid external dependencies +- When working with existing unittest code **When to use pytest:** -- For new projects -- When code conciseness is important -- When advanced features (fixtures, parameterization) are needed +- For new projects +- When code conciseness is important +- When advanced features (fixtures, parameterization) are needed ## Working with Complex Scenarios @@ -243,7 +287,7 @@ Unittest can handle more complex testing scenarios: ### Testing Classes -```python +```python-interactive import unittest class Calculator: @@ -287,13 +331,20 @@ class TestCalculator(unittest.TestCase): if __name__ == '__main__': unittest.main() + +... +---------------------------------------------------------------------- +Ran 4 tests in 0.001s + +OK + ``` ### Mocking with unittest.mock The `unittest.mock` module provides tools for simulating objects during testing: -```python +```python-interactive import unittest from unittest.mock import Mock, patch @@ -336,6 +387,13 @@ class TestWebService(unittest.TestCase): if __name__ == '__main__': unittest.main() + +... +---------------------------------------------------------------------- +Ran 2 tests in 0.001s + +OK + ``` ## Best Practices with unittest @@ -350,3 +408,11 @@ if __name__ == '__main__': ## Understanding Check **Which statement about the unittest module is most accurate?** + +1. unittest requires installing additional packages β€” Incorrect. unittest is a built-in module available in the Python standard library without additional installation. + +2. unittest allows writing tests as regular functions without classes β€” Incorrect. unittest uses an object-oriented approach, and tests must be written as methods in classes that inherit from unittest.TestCase. + +3. **Correct answer:** unittest provides assertion methods for checking expected results β€” Correct! unittest provides numerous assertion methods (such as assertEqual, assertTrue, etc.) through the TestCase class for verifying expected results. + +4. unittest has no capabilities for setting up and cleaning up the test environment β€” Incorrect. unittest provides setUp, tearDown, setUpClass, and tearDownClass methods for setting up and cleaning up the test environment. diff --git a/course/module-3/unittest-module/article.md b/course/module-3/unittest-module/article.md index ec5967f..c7b2d66 100644 --- a/course/module-3/unittest-module/article.md +++ b/course/module-3/unittest-module/article.md @@ -1,10 +1,16 @@ +--- +meta: + title: "The unittest module: the classic test framework" + description: "unittest is the built-in xUnit framework in Python: TestCase, assertEqual/assertTrue methods, setUp/tearDown. What you meet in legacy code." +--- + # The unittest module: the classic framework -Python's standard library has its own testing framework: `unittest`. The style is classic OOP: tests live in classes, inherit from `TestCase`, and use special `assert*` methods (if you've used JUnit in Java, this will look familiar). Most new projects pick `pytest`, but `unittest` is still around: +You open an older codebase, and the tests don't look like the ones you just wrote: classes, inheritance from `TestCase`, checks via `self.assertEqual` instead of a plain `assert`. That's `unittest` β€” the testing framework from Python's standard library, in classic OOP style (familiar to anyone who's seen JUnit in Java). Most new projects pick `pytest`, but `unittest` hasn't gone anywhere and still shows up: -- In legacy code (the framework's been there since Python 2.1). -- In projects that ban external dependencies. -- In tests of the standard library itself. +- In legacy code (the framework's been there since Python 2.1). +- In projects that ban external dependencies. +- In tests of the standard library itself. Knowing the basics is useful: sooner or later you'll open someone else's repo with `unittest` and need to navigate it. @@ -29,10 +35,10 @@ if __name__ == "__main__": What differs from pytest: -- Tests live in a **class** that inherits from `unittest.TestCase`. -- Method names start with `test_`, same as pytest. -- Instead of plain `assert`, you use **special methods**: `self.assertEqual(a, b)` instead of `assert a == b`. -- `unittest.main()` at the end is the entry point for `python test_file.py`. +- Tests live in a **class** that inherits from `unittest.TestCase`. +- Method names start with `test_`, same as pytest. +- Instead of plain `assert`, you use **special methods**: `self.assertEqual(a, b)` instead of `assert a == b`. +- `unittest.main()` at the end is the entry point for `python test_file.py`. Run via plain `python` or `python -m unittest`: @@ -46,14 +52,14 @@ python -m unittest test_addition.py `TestCase` ships with many specialized assertions. Six come up daily: -| Method | Checks | -| ---------------------------- | ---------------------------- | -| `assertEqual(a, b)` | `a == b` | -| `assertNotEqual(a, b)` | `a != b` | -| `assertTrue(x)` | `bool(x) is True` | -| `assertFalse(x)` | `bool(x) is False` | -| `assertIn(item, container)` | `item in container` | -| `assertRaises(Exception)` | block raises the exception | +| Method | Checks | +| --------------------------- | -------------------------- | +| `assertEqual(a, b)` | `a == b` | +| `assertNotEqual(a, b)` | `a != b` | +| `assertTrue(x)` | `bool(x) is True` | +| `assertFalse(x)` | `bool(x) is False` | +| `assertIn(item, container)` | `item in container` | +| `assertRaises(Exception)` | block raises the exception | Example of `assertRaises` via a context manager: @@ -95,17 +101,19 @@ class TestUserStorage(unittest.TestCase): self.assertEqual(self.users["id"], 1) ``` +![Illustration: class TestUserStorage(unittest.TestCase) containing a numbered sequence: 1.setUp 2.test_has_name 3.tearDown 4.setUp 5.test_has_id 6.tearDown; setUp/tearDown run again for each test](https://python-academy.org/static/guidePage/unittest-module/setup-teardown-en.webp "setUp and tearDown run for EVERY test") + For each test the cycle `setUp β†’ test_X β†’ tearDown` restarts; that guarantees tests are independent. There are also `setUpClass`/`tearDownClass` for one-time-per-class setup, but those are special cases. ## unittest vs pytest -| What | unittest | pytest | -| ------------------- | ------------------------------ | --------------------------------- | -| Tests | methods of a `TestCase` class | top-level functions | -| Assertions | `self.assertEqual()` etc. | plain `assert` | -| Setup | `setUp` / `tearDown` | fixtures with DI and scope | -| Parametrization | manual or via extensions | `@pytest.mark.parametrize` | -| Dependencies | built in | `pip install pytest` | +| What | unittest | pytest | +| --------------- | ----------------------------- | -------------------------- | +| Tests | methods of a `TestCase` class | top-level functions | +| Assertions | `self.assertEqual()` etc. | plain `assert` | +| Setup | `setUp` / `tearDown` | fixtures with DI and scope | +| Parametrization | manual or via extensions | `@pytest.mark.parametrize` | +| Dependencies | built in | `pip install pytest` | pytest is more concise and more flexible, and is the default for new projects. But `unittest` is built in and needs no install; for scripts and stdlib tests that's a plus. @@ -124,11 +132,16 @@ class TestUserAPI(unittest.TestCase): # ... testing code that uses requests.get ``` -## What's next? +## Understanding check -Next (and last article in the testing module): how to measure **coverage** with `pytest-cov` and automate test runs with **CI** (using GitHub Actions as an example). +**What is true about the unittest module?** ---- +1. **Correct answer:** Test classes in unittest must inherit from unittest.TestCase β€” TestCase is what gives you the assert\* methods, setUp/tearDown, and the runner integration. -**What is true about the unittest module?** +2. unittest uses plain assert for checks β€” No, unittest uses specialized methods (self.assertEqual, self.assertTrue etc.). Plain assert is the pytest style. + +3. setUp runs once before all tests in the class β€” setUp runs before every single test. Once-per-class is setUpClass. +4. Test methods in unittest can be named anything β€” The runner only picks up methods whose name starts with test. A method with any other name simply will not run as a test. + +Next (and last article in the testing module): how to measure **coverage** with `pytest-cov` and automate test runs with **CI** (using GitHub Actions as an example). diff --git a/course/module-3/why-tests-are-needed/article.md b/course/module-3/why-tests-are-needed/article.md index d44e0fa..b488c4a 100644 --- a/course/module-3/why-tests-are-needed/article.md +++ b/course/module-3/why-tests-are-needed/article.md @@ -1,3 +1,9 @@ +--- +meta: + title: "Why Tests Are Needed" + description: "Understanding the importance of testing in software development: benefits, real-world examples, and the costs of not testing." +--- + # Why Tests Are Needed Ever sent a message with an embarrassing typo? 😳 Imagine if your code had the same problem, but instead of a little embarrassment, it caused a rocket to crash or a bank transaction to fail! This is why we test our code - to catch mistakes before they have serious consequences. @@ -23,7 +29,7 @@ The later a bug is found, the more expensive it is to fix: Tests help ensure your code works correctly and reliably. -```python +```python-interactive def divide(a, b): return a / b @@ -33,6 +39,9 @@ try: print(f"Result: {result}") except ZeroDivisionError as e: print(f"Error occurred: {e}") + +Error occurred: division by zero + # With proper testing, you catch this before it happens in production def safe_divide(a, b): @@ -42,6 +51,10 @@ def safe_divide(a, b): print(safe_divide(10, 2)) print(safe_divide(10, 0)) + +5.0 +Cannot divide by zero + ``` ### 2. Documentation @@ -61,12 +74,15 @@ def test_user_creation(): Have you ever been afraid to change code because you might break something? With tests, you can make changes confidently ✨. -```python +```python-interactive # Original function def get_full_name(first, last): return f"{first} {last}" print(get_full_name("John", "Doe")) + +John Doe + # Enhanced function with tests to ensure it still works def get_full_name_v2(first, last, middle=None): @@ -84,6 +100,10 @@ for case in test_cases: middle = case.get("middle") result = get_full_name_v2(case["first"], case["last"], middle) print(f"Expected: {case['expected']}, Got: {result}, Passed: {result == case['expected']}") + +Expected: John Doe, Got: John Doe, Passed: True +Expected: Jane Elizabeth Smith, Got: Jane Elizabeth Smith, Passed: True + ``` ### 4. Preventing regressions @@ -125,20 +145,20 @@ def process_payment(payment_info): Sometimes we skip testing because we're in a hurry or don't see the value. But consider these real consequences: -- **Financial losses**: A bank's software bug could transfer millions to the wrong accounts -- **Reputation damage**: A social media app that accidentally makes private posts public -- **Security breaches**: Untested code might contain vulnerabilities that hackers can exploit -- **Lost productivity**: Developers spend more time fixing bugs than building new features -- **Stress and burnout**: The constant pressure of dealing with production emergencies +- **Financial losses**: A bank's software bug could transfer millions to the wrong accounts +- **Reputation damage**: A social media app that accidentally makes private posts public +- **Security breaches**: Untested code might contain vulnerabilities that hackers can exploit +- **Lost productivity**: Developers spend more time fixing bugs than building new features +- **Stress and burnout**: The constant pressure of dealing with production emergencies ## How much testing is enough? There's no one-size-fits-all answer, but here are some guidelines: -- **Critical systems** (medical, financial): Aim for 90%+ code coverage -- **Business applications**: 70-80% code coverage is often reasonable -- **Prototypes/MVPs**: Focus on testing core functionality -- **Open source**: Enough tests to give contributors confidence they haven't broken anything +- **Critical systems** (medical, financial): Aim for 90%+ code coverage +- **Business applications**: 70-80% code coverage is often reasonable +- **Prototypes/MVPs**: Focus on testing core functionality +- **Open source**: Enough tests to give contributors confidence they haven't broken anything Remember that test quality matters more than quantity! A few well-designed tests are better than many poorly written ones. @@ -146,14 +166,21 @@ Remember that test quality matters more than quantity! A few well-designed tests **Which of the following is NOT a benefit of writing tests?** +1. **Correct answer:** Tests eliminate the need for code reviews β€” Tests complement code reviews but don't replace them. Both are important quality tools. + +2. Tests act as documentation for how code should work β€” Tests do serve as executable documentation, showing how functions are expected to behave. + +3. Tests help catch bugs before they reach production β€” This is one of the main benefits of testing - finding issues early. + +4. Tests make code changes safer and easier β€” With a good test suite, you can refactor with confidence that you haven't broken functionality. ## A testing mindset Beyond just writing tests, developing a "testing mindset" can make you a better programmer: -- Think about edge cases before writing code -- Consider what could go wrong, not just what should go right -- Write code with testability in mind -- Use assertions to document assumptions +- Think about edge cases before writing code +- Consider what could go wrong, not just what should go right +- Write code with testability in mind +- Use assertions to document assumptions In the next lesson, we'll dive into `unittest` - Python's built-in framework for writing and running tests. diff --git a/interviewQuestions/1/en.mdx b/interviewQuestions/1/en.mdx index cbf0b1c..aec2284 100644 --- a/interviewQuestions/1/en.mdx +++ b/interviewQuestions/1/en.mdx @@ -1,22 +1,22 @@ --- meta: - title: 'What is Python and what are its key features?' - description: 'Python is a high-level interpreted language with dynamic typing, automatic memory management, and cross-platform support.' + title: "What is Python and what are its key features?" + description: "Python is a high-level interpreted language with dynamic typing, automatic memory management, and cross-platform support." --- > **Python** is a high-level, interpreted, general-purpose programming language with a focus on code readability and simple syntax. ## Key features: -- **Interpreted**: code is executed line by line by the interpreter, without the need for compilation. -- **Dynamic typing**: variable types are determined automatically at runtime, not at declaration. -- **Automatic memory management**: a built-in Garbage Collector frees unused memory. -- **Cross-platform**: code runs on Windows, macOS, and Linux without modifications. -- **Multi-paradigm**: supports object-oriented, functional, and procedural programming styles. -- **Extensive standard library**: modules for working with files, networking, JSON, and much more. +- **Interpreted**: code is executed line by line by the interpreter, without the need for compilation. +- **Dynamic typing**: variable types are determined automatically at runtime, not at declaration. +- **Automatic memory management**: a built-in Garbage Collector frees unused memory. +- **Cross-platform**: code runs on Windows, macOS, and Linux without modifications. +- **Multi-paradigm**: supports object-oriented, functional, and procedural programming styles. +- **Extensive standard library**: modules for working with files, networking, JSON, and much more. ## Where it's used: -- **Web development**: Django, Flask, FastAPI. -- **Data analysis and ML**: NumPy, Pandas, scikit-learn. -- **Automation and scripting**: file processing, DevOps, testing. +- **Web development**: Django, Flask, FastAPI. +- **Data analysis and ML**: NumPy, Pandas, scikit-learn. +- **Automation and scripting**: file processing, DevOps, testing. diff --git a/interviewQuestions/10/en.mdx b/interviewQuestions/10/en.mdx index 4694bcd..0d5b8a3 100644 --- a/interviewQuestions/10/en.mdx +++ b/interviewQuestions/10/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What kind of loops exist in Python and how do they differ?' - description: 'Python has for and while loops. Learn about break, continue, the else clause in loops, and the range() function.' + title: "What kind of loops exist in Python and how do they differ?" + description: "Python has for and while loops. Learn about break, continue, the else clause in loops, and the range() function." --- ## for loop: @@ -31,8 +31,8 @@ while count < 3: ## Control statements: -- `break` β€” terminates the loop. -- `continue` β€” skips to the next iteration. +- `break` β€” terminates the loop. +- `continue` β€” skips to the next iteration. ```python for i in range(10): diff --git a/interviewQuestions/11/en.mdx b/interviewQuestions/11/en.mdx index 9fbac4a..11a3139 100644 --- a/interviewQuestions/11/en.mdx +++ b/interviewQuestions/11/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is a function in Python and how do you define it?' - description: 'A function is a named block of code defined using def. Learn about parameters, return values, and default arguments.' + title: "What is a function in Python and how do you define it?" + description: "A function is a named block of code defined using def. Learn about parameters, return values, and default arguments." --- > A **function** is a named block of code that can be called multiple times. Functions help avoid duplication and make the code more readable. diff --git a/interviewQuestions/12/en.mdx b/interviewQuestions/12/en.mdx index 48cf102..1d33852 100644 --- a/interviewQuestions/12/en.mdx +++ b/interviewQuestions/12/en.mdx @@ -1,10 +1,10 @@ --- meta: - title: 'What are *args and **kwargs?' - description: '*args accepts an arbitrary number of positional arguments, **kwargs β€” keyword arguments. Learn how to use argument unpacking in Python.' + title: "What are *args and **kwargs?" + description: "*args accepts an arbitrary number of positional arguments, **kwargs β€” keyword arguments. Learn how to use argument unpacking in Python." --- -## *args β€” arbitrary number of positional arguments: +## \*args β€” arbitrary number of positional arguments: Collects all extra positional arguments into a **tuple**: @@ -16,7 +16,7 @@ total(1, 2, 3) # 6 total(10, 20) # 30 ``` -## **kwargs β€” arbitrary number of keyword arguments: +## \*\*kwargs β€” arbitrary number of keyword arguments: Collects all extra keyword arguments into a **dictionary**: diff --git a/interviewQuestions/13/en.mdx b/interviewQuestions/13/en.mdx index ac07857..9a1eee5 100644 --- a/interviewQuestions/13/en.mdx +++ b/interviewQuestions/13/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is an anonymous function (lambda)?' - description: 'Lambda is an anonymous one-line function. Learn lambda syntax, its usage with sorted, map, filter, and its limitations.' + title: "What is an anonymous function (lambda)?" + description: "Lambda is an anonymous one-line function. Learn lambda syntax, its usage with sorted, map, filter, and its limitations." --- > **Lambda** is an anonymous (unnamed) function defined in a single line. It can take any number of arguments but contains only one expression. @@ -46,6 +46,6 @@ evens = list(filter(lambda x: x % 2 == 0, numbers)) ## Limitations: -- Only **one expression** β€” you cannot use multi-line logic, loops, or assignments. -- Reduces **readability** for complex expressions β€” it's better to use a regular function. -- No **name** β€” makes debugging harder (appears as `` in the traceback). +- Only **one expression** β€” you cannot use multi-line logic, loops, or assignments. +- Reduces **readability** for complex expressions β€” it's better to use a regular function. +- No **name** β€” makes debugging harder (appears as `` in the traceback). diff --git a/interviewQuestions/14/en.mdx b/interviewQuestions/14/en.mdx index f0ca100..d1984cb 100644 --- a/interviewQuestions/14/en.mdx +++ b/interviewQuestions/14/en.mdx @@ -1,17 +1,17 @@ --- meta: - title: 'What are variable scopes in Python?' - description: 'Scopes in Python are determined by the LEGB rule: Local, Enclosing, Global, Built-in. Learn about the global and nonlocal keywords.' + title: "What are variable scopes in Python?" + description: "Scopes in Python are determined by the LEGB rule: Local, Enclosing, Global, Built-in. Learn about the global and nonlocal keywords." --- > In Python, scope determines where a variable is accessible. Python uses the **LEGB** rule for resolving variable names. ## The LEGB Rule: -- **L β€” Local**: variables inside the current function. -- **E β€” Enclosing**: variables in the outer (enclosing) function. -- **G β€” Global**: variables at the module level. -- **B β€” Built-in**: Python's built-in names (`print`, `len`, `range`). +- **L β€” Local**: variables inside the current function. +- **E β€” Enclosing**: variables in the outer (enclosing) function. +- **G β€” Global**: variables at the module level. +- **B β€” Built-in**: Python's built-in names (`print`, `len`, `range`). ```python x = "global" # Global diff --git a/interviewQuestions/15/en.mdx b/interviewQuestions/15/en.mdx index 516a65d..daed0a8 100644 --- a/interviewQuestions/15/en.mdx +++ b/interviewQuestions/15/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is list comprehension and how to use it?' - description: 'List comprehension is a compact syntax for creating lists in Python. Learn about conditions, nested comprehensions, and comparisons with loops.' + title: "What is list comprehension and how to use it?" + description: "List comprehension is a compact syntax for creating lists in Python. Learn about conditions, nested comprehensions, and comparisons with loops." --- > **List comprehension** is a concise way to create a new list from an existing collection in a single line. diff --git a/interviewQuestions/16/en.mdx b/interviewQuestions/16/en.mdx index d78c0cc..f38168c 100644 --- a/interviewQuestions/16/en.mdx +++ b/interviewQuestions/16/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What are generators and how do they differ from lists?' - description: 'Generators use yield for lazy evaluation and save memory. Learn the difference between generators and lists in Python.' + title: "What are generators and how do they differ from lists?" + description: "Generators use yield for lazy evaluation and save memory. Learn the difference between generators and lists in Python." --- > A **generator** is a function that returns items one by one using `yield`, rather than creating the entire list in memory all at once. @@ -33,9 +33,9 @@ squares_gen = (x ** 2 for x in range(1000000)) ## Key differences from a list: -- **Memory**: a generator only stores the current item, not the entire collection. -- **Single use**: a generator can be iterated over only once. -- **Laziness**: items are evaluated on demand, not in advance. +- **Memory**: a generator only stores the current item, not the entire collection. +- **Single use**: a generator can be iterated over only once. +- **Laziness**: items are evaluated on demand, not in advance. ```python gen = (x for x in range(3)) @@ -45,5 +45,5 @@ print(list(gen)) # [] β€” already exhausted ## When to use: -- **Generator** β€” when working with large datasets, streams, or when there is no need to keep all items. -- **List** β€” when multiple access, indexing is required, or the dataset is known to be small. +- **Generator** β€” when working with large datasets, streams, or when there is no need to keep all items. +- **List** β€” when multiple access, indexing is required, or the dataset is known to be small. diff --git a/interviewQuestions/17/en.mdx b/interviewQuestions/17/en.mdx index c70fc4c..6fc6216 100644 --- a/interviewQuestions/17/en.mdx +++ b/interviewQuestions/17/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'How do the built-in map, filter, and zip functions work?' - description: 'map applies a function to each item, filter selects by condition, zip combines multiple collections. Examples of usage in Python.' + title: "How do the built-in map, filter, and zip functions work?" + description: "map applies a function to each item, filter selects by condition, zip combines multiple collections. Examples of usage in Python." --- ## map β€” applies a function to each item: @@ -42,6 +42,6 @@ user_ages = dict(zip(names, ages)) ## Features: -- All three functions return **iterators**, not lists β€” you need to wrap them in `list()` to get a list. -- `zip` stops according to the **shortest** collection. -- In most cases, **list comprehension** is considered a more readable alternative to `map` and `filter`. +- All three functions return **iterators**, not lists β€” you need to wrap them in `list()` to get a list. +- `zip` stops according to the **shortest** collection. +- In most cases, **list comprehension** is considered a more readable alternative to `map` and `filter`. diff --git a/interviewQuestions/18/en.mdx b/interviewQuestions/18/en.mdx index 6d82474..73e6144 100644 --- a/interviewQuestions/18/en.mdx +++ b/interviewQuestions/18/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is unpacking in Python?' - description: 'Unpacking allows assigning elements of a collection to multiple variables. Learn about multiple assignment and the * and ** operators in Python.' + title: "What is unpacking in Python?" + description: "Unpacking allows assigning elements of a collection to multiple variables. Learn about multiple assignment and the * and ** operators in Python." --- > **Unpacking** is a mechanism that allows you to "extract" a collection into separate variables. @@ -25,7 +25,7 @@ a, b = b, a print(a, b) # 2 1 ``` -## Unpacking with * (asterisk): +## Unpacking with \* (asterisk): Collects the "remaining" items into a list: diff --git a/interviewQuestions/19/en.mdx b/interviewQuestions/19/en.mdx index 665ada8..6f6ab46 100644 --- a/interviewQuestions/19/en.mdx +++ b/interviewQuestions/19/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'How does exception handling work in Python?' - description: 'Exception handling in Python is done via try/except/else/finally. Learn about the exception hierarchy and proper error handling.' + title: "How does exception handling work in Python?" + description: "Exception handling in Python is done via try/except/else/finally. Learn about the exception hierarchy and proper error handling." --- > An **exception** is an error that occurs during the execution of a program. Python allows catching and handling exceptions so the program doesn't crash. @@ -41,14 +41,14 @@ except (ValueError, TypeError) as e: ## Hierarchy of main exceptions: -- `BaseException` - - `Exception` β€” base class for most exceptions - - `ValueError` β€” invalid value - - `TypeError` β€” invalid type - - `KeyError` β€” key not found in dictionary - - `IndexError` β€” index out of range for a list - - `FileNotFoundError` β€” file not found - - `ZeroDivisionError` β€” division by zero +- `BaseException` + - `Exception` β€” base class for most exceptions + - `ValueError` β€” invalid value + - `TypeError` β€” invalid type + - `KeyError` β€” key not found in dictionary + - `IndexError` β€” index out of range for a list + - `FileNotFoundError` β€” file not found + - `ZeroDivisionError` β€” division by zero ## Important rule: diff --git a/interviewQuestions/2/en.mdx b/interviewQuestions/2/en.mdx index d364c3a..903ef21 100644 --- a/interviewQuestions/2/en.mdx +++ b/interviewQuestions/2/en.mdx @@ -1,28 +1,28 @@ --- meta: - title: 'What are the basic data types in Python?' - description: 'Basic data types in Python: int, float, str, bool, and None. Learn their features and type conversion methods.' + title: "What are the basic data types in Python?" + description: "Basic data types in Python: int, float, str, bool, and None. Learn their features and type conversion methods." --- Python provides several built-in data types: ## Numeric types: -- `int` β€” integers of arbitrary precision: `42`, `-7`, `1_000_000` -- `float` β€” floating-point numbers: `3.14`, `-0.5`, `1e10` -- `complex` β€” complex numbers: `3+4j` +- `int` β€” integers of arbitrary precision: `42`, `-7`, `1_000_000` +- `float` β€” floating-point numbers: `3.14`, `-0.5`, `1e10` +- `complex` β€” complex numbers: `3+4j` ## Text type: -- `str` β€” character string: `"Hello"`, `'Python'` +- `str` β€” character string: `"Hello"`, `'Python'` ## Boolean type: -- `bool` β€” takes values `True` or `False` +- `bool` β€” takes values `True` or `False` ## Special type: -- `NoneType` β€” the single value `None`, meaning the absence of a value +- `NoneType` β€” the single value `None`, meaning the absence of a value ## Type conversion: diff --git a/interviewQuestions/20/en.mdx b/interviewQuestions/20/en.mdx index 08a048c..3b7ffd2 100644 --- a/interviewQuestions/20/en.mdx +++ b/interviewQuestions/20/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'How to create a custom exception?' - description: 'Custom exceptions are created by inheriting from Exception. Learn how and when to create your own exceptions in Python.' + title: "How to create a custom exception?" + description: "Custom exceptions are created by inheriting from Exception. Learn how and when to create your own exceptions in Python." --- > Custom exceptions are created by inheriting from the `Exception` class (or its subclasses). @@ -41,9 +41,9 @@ except ValidationError as e: ## When to create custom exceptions: -- When built-in exceptions don't describe the error accurately enough. -- To separate business logic errors (e.g., `UserNotFoundError`, `PermissionDeniedError`). -- To easily catch a group of related errors using a common base class. +- When built-in exceptions don't describe the error accurately enough. +- To separate business logic errors (e.g., `UserNotFoundError`, `PermissionDeniedError`). +- To easily catch a group of related errors using a common base class. ```python class AppError(Exception): diff --git a/interviewQuestions/21/en.mdx b/interviewQuestions/21/en.mdx index ed662d1..199e475 100644 --- a/interviewQuestions/21/en.mdx +++ b/interviewQuestions/21/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is a context manager and how does the with statement work?' - description: 'A context manager automatically handles resources via the with statement. Learn about the __enter__ and __exit__ methods in Python.' + title: "What is a context manager and how does the with statement work?" + description: "A context manager automatically handles resources via the with statement. Learn about the __enter__ and __exit__ methods in Python." --- > A **context manager** is an object that automatically performs actions when entering a block of code and when exiting it (even if an error occurs). @@ -26,8 +26,8 @@ with open("data.txt", "r") as file: ## How it works: -- `__enter__()` β€” called upon entering the `with` block. Its return value is assigned to the variable after `as`. -- `__exit__()` β€” called upon exiting the `with` block, **even if an exception was raised**. +- `__enter__()` β€” called upon entering the `with` block. Its return value is assigned to the variable after `as`. +- `__exit__()` β€” called upon exiting the `with` block, **even if an exception was raised**. ## Creating a custom context manager: @@ -51,7 +51,7 @@ with Timer(): ## Where context managers are used: -- Working with **files** (`open`) -- **Locks** in multithreading (`threading.Lock`) -- **Database connections** (automatic commit/rollback) -- **Temporary resources** (temporary files, network connections) +- Working with **files** (`open`) +- **Locks** in multithreading (`threading.Lock`) +- **Database connections** (automatic commit/rollback) +- **Temporary resources** (temporary files, network connections) diff --git a/interviewQuestions/22/en.mdx b/interviewQuestions/22/en.mdx index 7e81108..9d242d2 100644 --- a/interviewQuestions/22/en.mdx +++ b/interviewQuestions/22/en.mdx @@ -1,17 +1,17 @@ --- meta: - title: 'How to read and write files in Python?' - description: 'To work with files in Python, use the open() function with different modes. Learn about reading, writing, encodings, and the with statement.' + title: "How to read and write files in Python?" + description: "To work with files in Python, use the open() function with different modes. Learn about reading, writing, encodings, and the with statement." --- ## Opening a file: The `open()` function takes the path to the file and a mode: -- `'r'` β€” read (default) -- `'w'` β€” write (overwrites the file) -- `'a'` β€” append (adds to the end of the file) -- `'rb'` / `'wb'` β€” read / write in binary mode +- `'r'` β€” read (default) +- `'w'` β€” write (overwrites the file) +- `'a'` β€” append (adds to the end of the file) +- `'rb'` / `'wb'` β€” read / write in binary mode ## Reading a file: diff --git a/interviewQuestions/23/en.mdx b/interviewQuestions/23/en.mdx index 7385591..684cb00 100644 --- a/interviewQuestions/23/en.mdx +++ b/interviewQuestions/23/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What are classes and objects in Python?' - description: 'A class is a blueprint for creating objects. Learn about __init__, self, instance attributes, and class attributes in Python.' + title: "What are classes and objects in Python?" + description: "A class is a blueprint for creating objects. Learn about __init__, self, instance attributes, and class attributes in Python." --- > A **class** is a template (blueprint) for creating objects. An **object** is a specific instance of a class. @@ -37,9 +37,9 @@ print(dog1.species) # Canis familiaris ## self: -- `self` is a reference to the current instance of the class. -- It is passed automatically when a method is called. -- Through `self`, you can access the object's attributes and methods. +- `self` is a reference to the current instance of the class. +- It is passed automatically when a method is called. +- Through `self`, you can access the object's attributes and methods. ## Class attributes vs instance attributes: diff --git a/interviewQuestions/24/en.mdx b/interviewQuestions/24/en.mdx index 6c89ed1..5df8f21 100644 --- a/interviewQuestions/24/en.mdx +++ b/interviewQuestions/24/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is inheritance and how does it work in Python?' - description: 'Inheritance allows you to create classes based on existing ones. Learn about super(), method overriding, and multiple inheritance in Python.' + title: "What is inheritance and how does it work in Python?" + description: "Inheritance allows you to create classes based on existing ones. Learn about super(), method overriding, and multiple inheritance in Python." --- > **Inheritance** is an OOP mechanism where a child class inherits attributes and methods from a parent class. diff --git a/interviewQuestions/25/en.mdx b/interviewQuestions/25/en.mdx index 25172ef..344482e 100644 --- a/interviewQuestions/25/en.mdx +++ b/interviewQuestions/25/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is encapsulation in Python?' - description: 'Encapsulation restricts access to an object.s internal data. Learn about naming conventions _, __, name mangling, and @property in Python.' + title: "What is encapsulation in Python?" + description: "Encapsulation restricts access to an object.s internal data. Learn about naming conventions _, __, name mangling, and @property in Python." --- > **Encapsulation** is an OOP principle where an object's internal data is hidden from direct outside access. @@ -10,9 +10,9 @@ meta: In Python, there are no strict access modifiers (`private`, `public`). Instead, conventions are used: -- `name` β€” public attribute, accessible to everyone. -- `_name` β€” "protected" (by convention), intended for internal use only. -- `__name` β€” "private", Python applies **name mangling** (changing the name). +- `name` β€” public attribute, accessible to everyone. +- `_name` β€” "protected" (by convention), intended for internal use only. +- `__name` β€” "private", Python applies **name mangling** (changing the name). ```python class BankAccount: diff --git a/interviewQuestions/26/en.mdx b/interviewQuestions/26/en.mdx index 03ec992..15c6259 100644 --- a/interviewQuestions/26/en.mdx +++ b/interviewQuestions/26/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is polymorphism in Python?' - description: 'Polymorphism allows objects of different classes to share the same interface. Learn about duck typing and method overriding in Python.' + title: "What is polymorphism in Python?" + description: "Polymorphism allows objects of different classes to share the same interface. Learn about duck typing and method overriding in Python." --- > **Polymorphism** is the ability of objects from different classes to respond differently to the same method call. diff --git a/interviewQuestions/27/en.mdx b/interviewQuestions/27/en.mdx index 010e08c..bc9dfa9 100644 --- a/interviewQuestions/27/en.mdx +++ b/interviewQuestions/27/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What are static and class methods?' - description: 'Static methods (@staticmethod) do not depend on the instance, class methods (@classmethod) receive the class as the first argument. A comparison in Python.' + title: "What are static and class methods?" + description: "Static methods (@staticmethod) do not depend on the instance, class methods (@classmethod) receive the class as the first argument. A comparison in Python." --- ## Regular method (instance method): @@ -49,6 +49,6 @@ print(MathUtils.is_even(4)) # True ## When to use which: -- **Instance method** β€” when you need access to the object's attributes (`self`). -- **@classmethod** β€” for alternative constructors or when modifying class attributes. -- **@staticmethod** β€” for utility functions that logically belong to the class but do not need access to the instance or the class. +- **Instance method** β€” when you need access to the object's attributes (`self`). +- **@classmethod** β€” for alternative constructors or when modifying class attributes. +- **@staticmethod** β€” for utility functions that logically belong to the class but do not need access to the instance or the class. diff --git a/interviewQuestions/28/en.mdx b/interviewQuestions/28/en.mdx index 0646f3b..e0e7040 100644 --- a/interviewQuestions/28/en.mdx +++ b/interviewQuestions/28/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What are magic (dunder) methods?' - description: 'Magic methods (__init__, __str__, __repr__, __eq__, etc.) define object behavior during standard operations in Python.' + title: "What are magic (dunder) methods?" + description: "Magic methods (__init__, __str__, __repr__, __eq__, etc.) define object behavior during standard operations in Python." --- > **Magic methods** (dunder methods, short for "double underscore") are special methods surrounded by double underscores that define the behavior of objects during built-in operations. @@ -47,11 +47,11 @@ Point(1, 2) < Point(3, 4) # True ## Other useful magic methods: -- `__len__` β€” behavior for `len(obj)` -- `__getitem__` β€” index access `obj[key]` -- `__contains__` β€” the `in` operator -- `__add__` β€” the `+` operator -- `__call__` β€” calling an object like a function `obj()` +- `__len__` β€” behavior for `len(obj)` +- `__getitem__` β€” index access `obj[key]` +- `__contains__` β€” the `in` operator +- `__add__` β€” the `+` operator +- `__call__` β€” calling an object like a function `obj()` ```python class Basket: diff --git a/interviewQuestions/29/en.mdx b/interviewQuestions/29/en.mdx index b6b81be..6b45877 100644 --- a/interviewQuestions/29/en.mdx +++ b/interviewQuestions/29/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What are decorators and how do they work?' - description: 'A decorator is a function that wraps another function to add behavior. Learn about creating decorators and @wraps in Python.' + title: "What are decorators and how do they work?" + description: "A decorator is a function that wraps another function to add behavior. Learn about creating decorators and @wraps in Python." --- > A **decorator** is a function that takes another function and returns a new one, extending its behavior without modifying the original code. @@ -63,8 +63,8 @@ print(add.__doc__) # Adds two numbers. ## Practical examples of decorators: -- Logging function calls -- Measuring execution time -- Caching results -- Access control/authentication -- Input validation +- Logging function calls +- Measuring execution time +- Caching results +- Access control/authentication +- Input validation diff --git a/interviewQuestions/3/en.mdx b/interviewQuestions/3/en.mdx index d3848a7..c606c87 100644 --- a/interviewQuestions/3/en.mdx +++ b/interviewQuestions/3/en.mdx @@ -1,14 +1,14 @@ --- meta: - title: 'What is the difference between a list and a tuple?' - description: 'A list is a mutable collection, a tuple is immutable. Learn the key differences between list and tuple in Python.' + title: "What is the difference between a list and a tuple?" + description: "A list is a mutable collection, a tuple is immutable. Learn the key differences between list and tuple in Python." --- ## List: -- **Mutable**: you can add, remove, and modify elements. -- Created using square brackets `[]`. -- Uses more memory due to resizing capability. +- **Mutable**: you can add, remove, and modify elements. +- Created using square brackets `[]`. +- Uses more memory due to resizing capability. ```python fruits = ["apple", "banana", "cherry"] @@ -18,10 +18,10 @@ fruits[0] = "orange" # Modify element ## Tuple: -- **Immutable**: cannot be changed after creation. -- Created using parentheses `()`. -- Faster and uses less memory. -- Can be used as a dictionary key (since it's hashable). +- **Immutable**: cannot be changed after creation. +- Created using parentheses `()`. +- Faster and uses less memory. +- Can be used as a dictionary key (since it's hashable). ```python point = (10, 20) @@ -33,5 +33,5 @@ locations = {(55.75, 37.62): "Moscow"} ## When to use which: -- `list` β€” when the collection will change (adding, removing elements). -- `tuple` β€” when data should be immutable (coordinates, configurations, dictionary keys). +- `list` β€” when the collection will change (adding, removing elements). +- `tuple` β€” when data should be immutable (coordinates, configurations, dictionary keys). diff --git a/interviewQuestions/30/en.mdx b/interviewQuestions/30/en.mdx index f17b0dd..4800a10 100644 --- a/interviewQuestions/30/en.mdx +++ b/interviewQuestions/30/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What are abstract classes and why are they needed?' - description: 'Abstract classes define an interface via @abstractmethod. Learn about the abc module and creating abstract classes in Python.' + title: "What are abstract classes and why are they needed?" + description: "Abstract classes define an interface via @abstractmethod. Learn about the abc module and creating abstract classes in Python." --- > An **abstract class** is a class that cannot be instantiated directly. It defines an interface (a set of mandatory methods) for its child classes. @@ -58,6 +58,6 @@ print(rect.perimeter()) # 16 ## Why they are needed: -- **Contract**: they guarantee that any child class implements all the required methods. -- **Documentation**: they clearly show what methods must be implemented. -- **Early error detection**: if an abstract method is missing in the child class, Python will raise a `TypeError` at instantiation, rather than failing later when the method is called. +- **Contract**: they guarantee that any child class implements all the required methods. +- **Documentation**: they clearly show what methods must be implemented. +- **Early error detection**: if an abstract method is missing in the child class, Python will raise a `TypeError` at instantiation, rather than failing later when the method is called. diff --git a/interviewQuestions/31/en.mdx b/interviewQuestions/31/en.mdx index 4122662..77782ab 100644 --- a/interviewQuestions/31/en.mdx +++ b/interviewQuestions/31/en.mdx @@ -1,6 +1,6 @@ --- meta: - title: 'How does importing modules work in Python?' + title: "How does importing modules work in Python?" description: 'Modules in Python are imported via import and from...import. Learn about packages, aliases, and the __name__ == "__main__" construct.' --- diff --git a/interviewQuestions/32/en.mdx b/interviewQuestions/32/en.mdx index 94bea5d..0ffec12 100644 --- a/interviewQuestions/32/en.mdx +++ b/interviewQuestions/32/en.mdx @@ -1,16 +1,16 @@ --- meta: - title: 'What is a virtual environment and why is it needed?' - description: 'A virtual environment isolates project dependencies. Learn about venv, pip, and requirements.txt for package management in Python.' + title: "What is a virtual environment and why is it needed?" + description: "A virtual environment isolates project dependencies. Learn about venv, pip, and requirements.txt for package management in Python." --- > A **virtual environment** is an isolated Python environment where packages are installed only for a specific project, without affecting the global installation. ## Why it's needed: -- Different projects might require **different versions** of the same library. -- To avoid **conflicts** between dependencies of different projects. -- To lock exact package versions for **reproducibility**. +- Different projects might require **different versions** of the same library. +- To avoid **conflicts** between dependencies of different projects. +- To lock exact package versions for **reproducibility**. ## Creation and usage (venv): diff --git a/interviewQuestions/33/en.mdx b/interviewQuestions/33/en.mdx index a92fecd..cad9508 100644 --- a/interviewQuestions/33/en.mdx +++ b/interviewQuestions/33/en.mdx @@ -1,17 +1,17 @@ --- meta: - title: 'How to work with JSON in Python?' - description: 'The json module allows serialization and deserialization of data. Learn about json.dumps(), json.loads(), json.dump(), and json.load() in Python.' + title: "How to work with JSON in Python?" + description: "The json module allows serialization and deserialization of data. Learn about json.dumps(), json.loads(), json.dump(), and json.load() in Python." --- > **JSON** (JavaScript Object Notation) is a text-based format for data exchange. Python provides a built-in `json` module for working with it. ## Main functions: -- `json.dumps()` β€” Python object β†’ JSON string -- `json.loads()` β€” JSON string β†’ Python object -- `json.dump()` β€” Python object β†’ JSON file -- `json.load()` β€” JSON file β†’ Python object +- `json.dumps()` β€” Python object β†’ JSON string +- `json.loads()` β€” JSON string β†’ Python object +- `json.dump()` β€” Python object β†’ JSON file +- `json.load()` β€” JSON file β†’ Python object ## Serialization (Python β†’ JSON): @@ -49,11 +49,11 @@ with open("data.json", "r", encoding="utf-8") as f: ## Type mapping: -| JSON | Python | -|-----------|---------| -| object | dict | -| array | list | -| string | str | -| number | int/float | +| JSON | Python | +| ---------- | ---------- | +| object | dict | +| array | list | +| string | str | +| number | int/float | | true/false | True/False | -| null | None | +| null | None | diff --git a/interviewQuestions/34/en.mdx b/interviewQuestions/34/en.mdx index 83fb3e4..c8a3870 100644 --- a/interviewQuestions/34/en.mdx +++ b/interviewQuestions/34/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What are type annotations in Python?' - description: 'Type annotations specify expected data types. Learn syntax, the typing module, and why type annotations are used in Python.' + title: "What are type annotations in Python?" + description: "Type annotations specify expected data types. Learn syntax, the typing module, and why type annotations are used in Python." --- > **Type annotations** are a way to declare the expected types for function arguments, return values, and variables. They do **not affect** the runtime behavior of the program but serve as helpful aids during development. @@ -46,10 +46,10 @@ def parse(value: Union[int, str]) -> str: ## Why use annotations: -- **Documentation**: the expected types are clear without reading the function body. -- **IDEs**: autocomplete and code suggestions work better. -- **Static analysis**: tools like `mypy` can catch type errors prior to running code. -- **Collaboration**: makes the codebase easier for other developers to understand. +- **Documentation**: the expected types are clear without reading the function body. +- **IDEs**: autocomplete and code suggestions work better. +- **Static analysis**: tools like `mypy` can catch type errors prior to running code. +- **Collaboration**: makes the codebase easier for other developers to understand. ## Important note: diff --git a/interviewQuestions/35/en.mdx b/interviewQuestions/35/en.mdx index 1d6e0f8..ce85084 100644 --- a/interviewQuestions/35/en.mdx +++ b/interviewQuestions/35/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is the difference between a shallow copy and a deep copy?' - description: 'A shallow copy copies references to nested objects, while a deep copy (deepcopy) creates a completely independent copy in Python.' + title: "What is the difference between a shallow copy and a deep copy?" + description: "A shallow copy copies references to nested objects, while a deep copy (deepcopy) creates a completely independent copy in Python." --- ## Shallow copy: @@ -53,8 +53,8 @@ copy6 = copy.copy(lst) ## When to use which: -- **Shallow** β€” when the collection only contains immutable elements (numbers, strings). -- **Deep** β€” when there are nested mutable objects (lists within lists, dictionaries within lists). +- **Shallow** β€” when the collection only contains immutable elements (numbers, strings). +- **Deep** β€” when there are nested mutable objects (lists within lists, dictionaries within lists). ```python # Shallow is sufficient diff --git a/interviewQuestions/36/en.mdx b/interviewQuestions/36/en.mdx index 32105c2..d400c09 100644 --- a/interviewQuestions/36/en.mdx +++ b/interviewQuestions/36/en.mdx @@ -1,15 +1,15 @@ --- meta: - title: 'What are iterators and the iteration protocol?' - description: 'An iterator is an object with __iter__ and __next__ methods. Learn how the iteration protocol works and what the for loop does under the hood in Python.' + title: "What are iterators and the iteration protocol?" + description: "An iterator is an object with __iter__ and __next__ methods. Learn how the iteration protocol works and what the for loop does under the hood in Python." --- > An **iterator** is an object that returns elements one by one using the `__next__()` method and signals completion by raising a `StopIteration` exception. ## The iteration protocol: -- `__iter__()` β€” returns the iterator object itself. -- `__next__()` β€” returns the next element or raises a `StopIteration` exception. +- `__iter__()` β€” returns the iterator object itself. +- `__next__()` β€” returns the next element or raises a `StopIteration` exception. ## How a for loop works: @@ -53,9 +53,9 @@ for num in Countdown(5): ## Built-in iterables: -- `list`, `tuple`, `str`, `dict`, `set` are **iterable** (they have an `__iter__` method). -- `range()`, `map()`, `filter()`, `zip()` return **iterators**. -- An **iterator** can only be traversed **once**, while an **iterable object** can be looped over multiple times. +- `list`, `tuple`, `str`, `dict`, `set` are **iterable** (they have an `__iter__` method). +- `range()`, `map()`, `filter()`, `zip()` return **iterators**. +- An **iterator** can only be traversed **once**, while an **iterable object** can be looped over multiple times. ## In practice: diff --git a/interviewQuestions/37/en.mdx b/interviewQuestions/37/en.mdx index 216e621..6bd1d39 100644 --- a/interviewQuestions/37/en.mdx +++ b/interviewQuestions/37/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is the difference between is and ==?' - description: 'The == operator compares values, while is checks object identity in memory. Learn about caching small numbers and the proper usage in Python.' + title: "What is the difference between is and ==?" + description: "The == operator compares values, while is checks object identity in memory. Learn about caching small numbers and the proper usage in Python." --- ## == (value comparison): @@ -46,7 +46,7 @@ print(x is y) # False β€” different objects ## Rules of usage: -- Use `is` only for comparison against `None`: +- Use `is` only for comparison against `None`: ```python # Correct @@ -58,5 +58,5 @@ if value == None: print("No value") ``` -- When comparing values, **always** use `==`. -- `is` only checks the `id()` of the objects β€” this is useful mostly for singletons like `None`, `True`, or `False`. +- When comparing values, **always** use `==`. +- `is` only checks the `id()` of the objects β€” this is useful mostly for singletons like `None`, `True`, or `False`. diff --git a/interviewQuestions/38/en.mdx b/interviewQuestions/38/en.mdx index 48abb1c..a84a947 100644 --- a/interviewQuestions/38/en.mdx +++ b/interviewQuestions/38/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'How does memory management work in Python?' - description: 'Python manages memory using reference counting and a garbage collector. Learn how Python automatically frees unused memory.' + title: "How does memory management work in Python?" + description: "Python manages memory using reference counting and a garbage collector. Learn how Python automatically frees unused memory." --- > Python automatically handles memory management, relieving developers of manual memory allocation and deallocation. @@ -41,7 +41,7 @@ To solve this, Python runs a **Garbage Collector** (the `gc` module) that discov ## Key takeaways: -- **Reference counting** is the primary mechanism and runs immediately. -- **Garbage collector** is a secondary mechanism designed to break circular references. -- **del** removes a reference to an object, not necessarily the object itself. -- Developers rarely need to get involved in memory management β€” Python handles it perfectly. +- **Reference counting** is the primary mechanism and runs immediately. +- **Garbage collector** is a secondary mechanism designed to break circular references. +- **del** removes a reference to an object, not necessarily the object itself. +- Developers rarely need to get involved in memory management β€” Python handles it perfectly. diff --git a/interviewQuestions/39/en.mdx b/interviewQuestions/39/en.mdx index e68e16f..9cd6910 100644 --- a/interviewQuestions/39/en.mdx +++ b/interviewQuestions/39/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is a closure in Python?' - description: 'A closure is a nested function that remembers variables from its enclosing scope. Learn about closures and their applications in Python.' + title: "What is a closure in Python?" + description: "A closure is a nested function that remembers variables from its enclosing scope. Learn about closures and their applications in Python." --- > A **closure** is a nested function that "remembers" variables from the enclosing function's scope, even after the enclosing function has finished its execution. @@ -23,9 +23,9 @@ print(triple(5)) # 15 ## Conditions for a closure: -- There must be a **nested function**. -- The nested function must refer to an environment variable from the **enclosing function**. -- The enclosing function must **return** the nested function. +- There must be a **nested function**. +- The nested function must refer to an environment variable from the **enclosing function**. +- The enclosing function must **return** the nested function. ## Practical applications: diff --git a/interviewQuestions/4/en.mdx b/interviewQuestions/4/en.mdx index 81d1edd..caf7922 100644 --- a/interviewQuestions/4/en.mdx +++ b/interviewQuestions/4/en.mdx @@ -1,14 +1,14 @@ --- meta: - title: 'What is the difference between set and frozenset?' - description: 'set is a mutable collection of unique elements, frozenset is immutable. Learn about set operations and key differences in Python.' + title: "What is the difference between set and frozenset?" + description: "set is a mutable collection of unique elements, frozenset is immutable. Learn about set operations and key differences in Python." --- ## set: -- A **mutable** unordered collection of unique elements. -- Supports adding and removing elements. -- Cannot be used as a dictionary key or element of another set. +- A **mutable** unordered collection of unique elements. +- Supports adding and removing elements. +- Cannot be used as a dictionary key or element of another set. ```python colors = {"red", "green", "blue"} @@ -18,9 +18,9 @@ colors.discard("red") ## frozenset: -- An **immutable** version of a set. -- Does not support adding or removing elements. -- Is hashable β€” can be used as a dictionary key. +- An **immutable** version of a set. +- Does not support adding or removing elements. +- Is hashable β€” can be used as a dictionary key. ```python immutable_set = frozenset([1, 2, 3]) diff --git a/interviewQuestions/40/en.mdx b/interviewQuestions/40/en.mdx index a44beff..eb1d29f 100644 --- a/interviewQuestions/40/en.mdx +++ b/interviewQuestions/40/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What is the difference between threading and multiprocessing in Python?' - description: 'threading is better suited for I/O bounds tasks, while multiprocessing is used for CPU-bound computation. Learn about the GIL and choosing the right concurrency model in Python.' + title: "What is the difference between threading and multiprocessing in Python?" + description: "threading is better suited for I/O bounds tasks, while multiprocessing is used for CPU-bound computation. Learn about the GIL and choosing the right concurrency model in Python." --- ## Multithreading (threading): @@ -48,17 +48,17 @@ for p in processes: The GIL is a CPython mechanism that allows **only one thread** to execute Python bytecode at a time. As a result: -- Threads **do not speed up** tasks performing heavy calculations (CPU-bound tasks). -- Threads **do speed up** tasks waiting on external events (I/O-bound tasks): network requests, reading files. +- Threads **do not speed up** tasks performing heavy calculations (CPU-bound tasks). +- Threads **do speed up** tasks waiting on external events (I/O-bound tasks): network requests, reading files. ## When to use which: -- **threading** β€” for I/O-bound tasks: file downloads, API calls, database queries. -- **multiprocessing** β€” for CPU-bound tasks: data processing, complex mathematical calculations, and algorithms. +- **threading** β€” for I/O-bound tasks: file downloads, API calls, database queries. +- **multiprocessing** β€” for CPU-bound tasks: data processing, complex mathematical calculations, and algorithms. -| Feature | threading | multiprocessing | -|--------------------|---------------|-------------------| -| Memory | Shared | Separate | -| GIL impact | Constrained | Unaffected | -| Overhead | Low | High | -| Best used for | I/O-bound | CPU-bound | +| Feature | threading | multiprocessing | +| ------------- | ----------- | --------------- | +| Memory | Shared | Separate | +| GIL impact | Constrained | Unaffected | +| Overhead | Low | High | +| Best used for | I/O-bound | CPU-bound | diff --git a/interviewQuestions/5/en.mdx b/interviewQuestions/5/en.mdx index bdf6648..f93eeb3 100644 --- a/interviewQuestions/5/en.mdx +++ b/interviewQuestions/5/en.mdx @@ -1,16 +1,16 @@ --- meta: - title: 'What is a dictionary (dict) and how does it work?' - description: 'A dictionary in Python is a key-value collection based on a hash table with O(1) access. Learn about main dict methods and features.' + title: "What is a dictionary (dict) and how does it work?" + description: "A dictionary in Python is a key-value collection based on a hash table with O(1) access. Learn about main dict methods and features." --- > **Dictionary (dict)** is a mutable collection of key-value pairs implemented using a hash table. ## Key characteristics: -- Access, insertion, and deletion are performed in **O(1)** on average. -- Keys must be **hashable** (strings, numbers, tuples). -- Since Python 3.7, dictionaries **preserve insertion order**. +- Access, insertion, and deletion are performed in **O(1)** on average. +- Keys must be **hashable** (strings, numbers, tuples). +- Since Python 3.7, dictionaries **preserve insertion order**. ```python user = { diff --git a/interviewQuestions/6/en.mdx b/interviewQuestions/6/en.mdx index d8c7d82..54b9d34 100644 --- a/interviewQuestions/6/en.mdx +++ b/interviewQuestions/6/en.mdx @@ -1,17 +1,17 @@ --- meta: - title: 'What are mutable and immutable data types?' - description: 'Mutable types (list, dict, set) can be changed after creation, immutable (int, str, tuple) cannot. Learn why this matters in Python.' + title: "What are mutable and immutable data types?" + description: "Mutable types (list, dict, set) can be changed after creation, immutable (int, str, tuple) cannot. Learn why this matters in Python." --- > In Python, all objects are divided into **mutable** and **immutable** depending on whether their contents can be changed after creation. ## Immutable: -- `int`, `float`, `bool` -- `str` -- `tuple` -- `frozenset` +- `int`, `float`, `bool` +- `str` +- `tuple` +- `frozenset` When "modified," a new object is created: @@ -24,9 +24,9 @@ print(id(x)) # Different id β€” this is a new object ## Mutable: -- `list` -- `dict` -- `set` +- `list` +- `dict` +- `set` The object is modified "in place": @@ -39,9 +39,9 @@ print(id(lst)) # Same id β€” object was modified ## Why this matters: -- **Dictionary keys** can only be immutable objects. -- **Passing to functions**: mutable objects can be changed inside a function, which may cause unexpected side effects. -- **Default values**: don't use mutable objects as default values in functions. +- **Dictionary keys** can only be immutable objects. +- **Passing to functions**: mutable objects can be changed inside a function, which may cause unexpected side effects. +- **Default values**: don't use mutable objects as default values in functions. ```python # Common mistake diff --git a/interviewQuestions/7/en.mdx b/interviewQuestions/7/en.mdx index aff034a..80c91fc 100644 --- a/interviewQuestions/7/en.mdx +++ b/interviewQuestions/7/en.mdx @@ -1,13 +1,13 @@ --- meta: - title: 'How do indexing and slicing work in Python?' - description: 'Indexing accesses elements by position, slicing extracts parts of a sequence. Learn the [start:stop:step] syntax in Python.' + title: "How do indexing and slicing work in Python?" + description: "Indexing accesses elements by position, slicing extracts parts of a sequence. Learn the [start:stop:step] syntax in Python." --- ## Indexing: -- Elements are numbered starting from `0`. -- Negative indices count from the end: `-1` is the last element. +- Elements are numbered starting from `0`. +- Negative indices count from the end: `-1` is the last element. ```python text = "Python" @@ -20,9 +20,9 @@ text[-2] # 'o' Syntax: `[start:stop:step]` -- `start` β€” starting index (inclusive), defaults to `0`. -- `stop` β€” ending index (exclusive), defaults to the length of the sequence. -- `step` β€” step size, defaults to `1`. +- `start` β€” starting index (inclusive), defaults to `0`. +- `stop` β€” ending index (exclusive), defaults to the length of the sequence. +- `step` β€” step size, defaults to `1`. ```python nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] diff --git a/interviewQuestions/8/en.mdx b/interviewQuestions/8/en.mdx index 23812e1..59f0eaf 100644 --- a/interviewQuestions/8/en.mdx +++ b/interviewQuestions/8/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'What are f-strings and how do they compare to other formatting methods?' - description: 'f-strings are the most convenient way to format strings in Python. Comparison of f-string, .format(), and % operator for value interpolation.' + title: "What are f-strings and how do they compare to other formatting methods?" + description: "f-strings are the most convenient way to format strings in Python. Comparison of f-string, .format(), and % operator for value interpolation." --- Python offers several ways to format strings: @@ -33,9 +33,9 @@ print("Hello, %s! You are %d years old." % (name, age)) ## Comparison: -- **f-strings** β€” the most readable and fastest method. Supports any expressions. -- **.format()** β€” useful when the string template is defined in advance. -- **%** β€” legacy approach, found in older code. +- **f-strings** β€” the most readable and fastest method. Supports any expressions. +- **.format()** β€” useful when the string template is defined in advance. +- **%** β€” legacy approach, found in older code. ## Number formatting: diff --git a/interviewQuestions/9/en.mdx b/interviewQuestions/9/en.mdx index 1273306..7992e46 100644 --- a/interviewQuestions/9/en.mdx +++ b/interviewQuestions/9/en.mdx @@ -1,7 +1,7 @@ --- meta: - title: 'How do if / elif / else conditional statements work?' - description: 'Conditional statements if/elif/else control program branching. Learn about the ternary operator and truthy/falsy values in Python.' + title: "How do if / elif / else conditional statements work?" + description: "Conditional statements if/elif/else control program branching. Learn about the ternary operator and truthy/falsy values in Python." --- Conditional statements allow you to execute different blocks of code depending on a condition. @@ -31,9 +31,9 @@ status = "adult" if age >= 18 else "minor" In Python, the following values are considered **false** (Falsy): -- `False`, `None` -- `0`, `0.0` -- Empty collections: `""`, `[]`, `()`, `{}`, `set()` +- `False`, `None` +- `0`, `0.0` +- Empty collections: `""`, `[]`, `()`, `{}`, `set()` Everything else is considered **true** (Truthy):