├── .gitignore ├── 00-basic-concepts ├── README.md └── images │ ├── Docker.png │ ├── cloud.png │ ├── container.webp │ ├── docker_image_container.png │ └── lifecycle.png ├── 01-enviroment-setup └── README.md ├── 02-hello_world ├── Dockerfile ├── README.md └── main.py ├── 03-python-programming ├── 01-first_hello_with_docker │ ├── Dockerfile │ ├── main.py │ └── readme.md ├── 02-variables_and_arithematic_operators │ ├── main.py │ └── readme.md ├── image.png └── readme.md ├── 04-class_work ├── 04a-pep8_guidelines │ └── readme.md ├── 04b-string_methods │ ├── readme.md │ └── string_methods.ipynb └── 04c-dev_container │ ├── .devcontainer │ └── devcontainer.json │ ├── .github │ └── dependabot.yml │ ├── Dockerfile │ └── main.py ├── 05-class ├── 05a-type_hints │ ├── main.py │ └── readme.md ├── 05b-more_concepts │ └── readme.md ├── 05c-lists │ └── README.md ├── 05d-nested-lists │ └── README.md └── code.ipynb ├── 06-control_flow ├── 06a_falsy_values │ └── readme.md ├── 06b_conditional_statements │ ├── code.ipynb │ └── readme.md ├── 06c_logical_operators │ └── readme.md ├── 06d_membership&identity_operatros │ ├── code.ipynb │ └── readme.md └── 06e_loops │ ├── 01_for_loop │ ├── README.md │ └── code.ipynb │ ├── 02_while_loop │ ├── README.md │ ├── code.ipynb │ └── main.py │ ├── 03_loop_control_statements │ ├── README.md │ └── code.ipynb │ └── README.md ├── 07-tuples_dictionaries ├── 07a-list_comprehension │ ├── code.ipynb │ └── readme.md ├── 07b-tuple │ ├── code.ipynb │ └── readme.md └── 07c-dictionary │ └── readme.md ├── 08-functions ├── 08a-function_parameters │ └── readme.md ├── 08b-function_return │ └── readme.md ├── 08c-postional_arguments │ └── readme.md ├── 08d-args_kwargs │ └── readme.md ├── 08e-keyword_arguments │ └── readme.md ├── 08f-scopes_in_python │ └── readme.md ├── 08g-recursive_functions │ └── readme.md ├── 08h- lambda_function │ └── readme.md ├── class_09.ipynb └── readme.md ├── 09-OOP ├── 09a-classes │ └── README.md ├── 09b-Objects │ └── README.md ├── 09c-inheritance │ ├── code.ipynb │ └── inheritance.ipynb ├── 09d-abstraction │ └── readme.md ├── 09e-encapsulation │ └── readme.md ├── 09f-class_level_att_methods │ └── readme.md ├── 09g-composition │ └── readme.md └── README.md ├── 10-version_contol_git └── readme.md ├── 11-exceptions └── readme.md ├── 12-modules ├── app.py ├── calculations.py └── readme.md ├── 13-Final_Projects └── project.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | .testmondata 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | # For a library or package, you might want to ignore these files since the code is intended to run in multiple environments; otherwise, check them in: 83 | # .python-version 84 | 85 | # pipenv 86 | # Only include the Pipfile.lock in version control if you want to lock your dependencies for reproducibility, as per pipenv documentation: 87 | # https://pipenv.readthedocs.io/en/latest/advanced/#pipfile-lock 88 | Pipfile.lock 89 | 90 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 91 | __pypackages__/ 92 | 93 | # Celery stuff 94 | celerybeat-schedule 95 | celerybeat.pid 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .env.* 103 | .venv 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | # pytype static type analyzer 128 | .pytype/ 129 | 130 | # Cython debug symbols 131 | cython_debug/ 132 | 133 | # macOS specific files 134 | .DS_Store 135 | .AppleDouble 136 | .LSOverride 137 | 138 | # Icon must end with two \r 139 | Icon\r\r 140 | 141 | # Thumbnails 142 | ._* 143 | 144 | # Files that might appear when using macOS 145 | .DS_Store 146 | .Spotlight-V100 147 | .Trashes 148 | ._* 149 | 150 | # macOS Finder directory 151 | .DS_Store 152 | -------------------------------------------------------------------------------- /00-basic-concepts/images/Docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usmanashrf/batch63-cloud-native-modern-python-classwork/7e44bb72d1ee0d7b263780ed67ca3d2564b06777/00-basic-concepts/images/Docker.png -------------------------------------------------------------------------------- /00-basic-concepts/images/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usmanashrf/batch63-cloud-native-modern-python-classwork/7e44bb72d1ee0d7b263780ed67ca3d2564b06777/00-basic-concepts/images/cloud.png -------------------------------------------------------------------------------- /00-basic-concepts/images/container.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usmanashrf/batch63-cloud-native-modern-python-classwork/7e44bb72d1ee0d7b263780ed67ca3d2564b06777/00-basic-concepts/images/container.webp -------------------------------------------------------------------------------- /00-basic-concepts/images/docker_image_container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usmanashrf/batch63-cloud-native-modern-python-classwork/7e44bb72d1ee0d7b263780ed67ca3d2564b06777/00-basic-concepts/images/docker_image_container.png -------------------------------------------------------------------------------- /00-basic-concepts/images/lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usmanashrf/batch63-cloud-native-modern-python-classwork/7e44bb72d1ee0d7b263780ed67ca3d2564b06777/00-basic-concepts/images/lifecycle.png -------------------------------------------------------------------------------- /01-enviroment-setup/README.md: -------------------------------------------------------------------------------- 1 | # Environment Setup 2 | We'll start with downloading and setting up the necessary software and tools. 3 | 4 | ## 1. Visual Studio Code 5 | Visual Studio Code (VSCode) is a free, open-source, cross-platform text editor that allows you to write code, edit code, and debug code. It is used for development, debugging, and version control. 6 | 7 | It is language agnostic and can be used for many different programming languages. It is free to download and install. 8 | 9 | [Download VSCode](https://code.visualstudio.com/download) 10 | 11 | ## 2. Docker Desktop 12 | Docker Desktop is a software application that allows to build, share, and run containerized applications and microservices on local machine. It provides a user-friendly interface for managing containers, images, and Docker Compose files. Essentially, it's a tool that makes working with Docker easier and more accessible for developers. 13 | 14 | Docker Desktop is free for education and learning purpose. However, commercial use of Docker Desktop at a company of more than 250 employees OR more than $10 million in annual revenue requires a paid subscription (Pro, Team, or Business). 15 | 16 | [Download Docker Desktop](https://www.docker.com/products/docker-desktop) and install. 17 | 18 | ### 2.1 Docker Desktop for Windows 19 | 20 | **Pre-requisites:** 21 | - Windows 10 or later (64-bit) 22 | - Enable Hardware Virtualization from BIOS Settings 23 | - Windows Subsystem for Linux (WSL) - *required to run linux containers* 24 | 25 | **Enable WSL:** 26 | 1. Open Start menu and search for "Turn Windows Features on or off". A new window will open. 27 | 2. Enable "Windows Subsystem for Linux (WSL)" and "Virtual Machine Platform". 28 | 3. Save it and restart the system. 29 | 4. To check the installed version of WSL, run `wsl -l -v` in the command prompt. 30 | 5. If you see version '1', we've to update to version '2'. In command prompt, run `wsl --update`. After update, run this command `wsl --set-default-version 2`. 31 | 32 | Read more about [How to install WSL.](https://learn.microsoft.com/en-us/windows/wsl/install) 33 | 34 | ### 2.2 Docker Desktop for Mac 35 | Check out [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install) 36 | 37 | ### 2.3 Docker Desktop for Linux 38 | Check out [Docker Desktop for Linux](https://docs.docker.com/desktop/install/linux-install/) 39 | 40 | ### 2.4 Run your first container 41 | After successful installation, run docker desktop. 42 | Open CLI tool and run this command `docker --version`. this will show the version of docker installed. 43 | 44 | In CLI, run `docker run hello-world` command. Read the output. -------------------------------------------------------------------------------- /02-hello_world/Dockerfile: -------------------------------------------------------------------------------- 1 | # base image 2 | FROM python:3.12-slim 3 | 4 | # setup working directory in container 5 | WORKDIR /app 6 | 7 | # copy all files to app directory 8 | COPY . /app/ 9 | 10 | # command to run on container start 11 | CMD ["python", "main.py"] 12 | -------------------------------------------------------------------------------- /02-hello_world/README.md: -------------------------------------------------------------------------------- 1 | # How to run this code 2 | ### Checking to see if Docker is running: 3 | ```bash 4 | docker version 5 | ``` 6 | ### Building the Image for Dev: 7 | ```bash 8 | docker build -f Dockerfile -t my-first-image . 9 | ``` 10 | ### Check Images: 11 | ```bash 12 | docker images 13 | ``` 14 | ### Running the Container: 15 | first way 16 | ```bash 17 | docker run --name first-cont1 my-first-image 18 | ``` 19 | second way 20 | ```bash 21 | docker run -d --name first-cont1 my-first-image 22 | ``` 23 | 24 | --- 25 | 26 | # Dockerfile Explanation 27 | 28 | ### Base Image: 29 | ```bash 30 | FROM python:3.12 31 | ``` 32 | This line tells Docker to use the Python 3.12 image as the base for your container. This image already includes a minimal operating system along with Python 3.12 installed. So, you don’t need to install an operating system separately. 33 | ### Set Up Working Directory: 34 | ```bash 35 | WORKDIR /app 36 | ``` 37 | This sets the working directory inside the container to /app. It’s like saying, “Whenever I do something in this container, let’s do it inside this /app folder.” 38 | 39 | ### Copy Files: 40 | ```bash 41 | COPY . /app/ 42 | ``` 43 | • This line copies all the files from your current directory on your computer (where the Dockerfile is) to the /app directory inside the container. It’s like packing all your project files and putting them in the /app folder inside the container. 44 | 45 | ### Command to Run: 46 | ```bash 47 | CMD ["python", "main.py"] 48 | ``` 49 | This specifies the command to run when the container starts. In this case, it runs python main.py, which means it will execute the main.py script using Python. It’s like saying, “When you turn on this computer, automatically run this Python script.” 50 | 51 | ## Putting It All Together 52 | 1. You start with a box that already has Python 3.12 installed (base image). 53 | 2. You decide that all work inside this box will happen in a specific area called /app (working directory). 54 | 3. You pack all your project files into this area (copy files). 55 | 4. You set the box to automatically run your main Python script (main.py) whenever it’s opened (command to run). 56 | -------------------------------------------------------------------------------- /02-hello_world/main.py: -------------------------------------------------------------------------------- 1 | '''Greetings from PIAIC''' 2 | 3 | print("Hello Class! Greetings from PIAIC.") 4 | -------------------------------------------------------------------------------- /03-python-programming/01-first_hello_with_docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # base image 2 | FROM python:3.12 3 | 4 | # setup working directory in container 5 | WORKDIR /app 6 | 7 | # copy all files to app directory 8 | COPY . /app/ 9 | 10 | # command to run on container start 11 | CMD ["python", "main.py"] -------------------------------------------------------------------------------- /03-python-programming/01-first_hello_with_docker/main.py: -------------------------------------------------------------------------------- 1 | print ("hello world") -------------------------------------------------------------------------------- /03-python-programming/01-first_hello_with_docker/readme.md: -------------------------------------------------------------------------------- 1 | # CLASS-03 PART-01 2 | 3 | ## Steps for developing an application to containerizing it. 4 | 5 | **We will create a simple python application which will print "hello world". Then will dockerize/containerize it.** 6 | 7 | **Step-01:** Create a directory(folder) in your local system for creating a new application. 8 | 9 | **Step-02:** Open directory in VSCode. You can open the directly in terminal and then write `code .`. This will open your VSCode in that directory. 10 | 11 | **Step-03:** Create a new python file `main.py` in that directory (within VSCode). 12 | 13 | **Step-04:** Write `print("hello world")` in the file. 14 | 15 | **Great! our python application is developed. Now we will dockerize it.** 16 | 17 | ### What is Dockerizing/Containerizing an Application? 18 | 19 | Dockerizing/Containerizing is the process of running a software application in a container. A container is an isolated, lightweight package of software/applicaton that includes everything needed to run an application. 20 | 21 | So to run a **container**, first we have to bundle our application in a single package. This package is called `image`. 22 | 23 | An **image** is essentially a snapshot of a container at a specific point in time. It includes your application, its dependencies, libraries, and configuration files. Once you have this image, you can create multiple instances of it, which are called **containers**. 24 | 25 | But wait here. **How do we create an image?** Docker asks the developers, hey developers! i will create an image of the application for you but first i need instructions from your side what this image will contain. 26 | 27 | _Here we have to provide the instructions to docker to create and image. We provide the instructions in a special file called `Dockerfile`_ 28 | 29 | _Let's start new steps for dockerizing our application._ 30 | 31 | **Step-01:** Create a new file `Dockerfile` in that directory (within VSCode). 32 | 33 | **Step-02:** Copy below code and paste in `Dockerfile`. We'll learn about this code in upcoming classes. 34 | 35 | ```dockerfile 36 | # base image 37 | FROM python:3.12 38 | 39 | # setup working directory in container 40 | WORKDIR /app 41 | 42 | # copy all files to app directory 43 | COPY . /app/ 44 | 45 | # command to run on container start 46 | CMD ["python", "main.py"] 47 | ``` 48 | 49 | **Step-03:** Now will write a command in CLI to create our image. 50 | 51 | ```bash 52 | docker build -t my_first_image . 53 | ``` 54 | 55 | - _`docker` this intialize the docker's command line tool_. 56 | 57 | - _`build` tells docker to build our image_. 58 | 59 | - _`-t` this is option/switch which is used to tell the docker that whatever comes after it is the name/tag of our image_. 60 | 61 | - _`my_first_image` the name/tag of our image_. 62 | 63 | - _`.` used to identify the location of our application files. Here it means the current directory_. 64 | 65 | Our image with name/tage `my_first_image` has been created. We can share it with anyone or deploy it anywhere. 66 | 67 | **Step-03:** Now we will run application in container using the image `my_first_image` we created. 68 | Here is the command to run the containter from the image. 69 | 70 | ```bash 71 | docker run my_first_image 72 | ``` 73 | 74 | - _`docker` this intialize the docker's command line tool_. 75 | 76 | - _`run` tells docker to run a container_. 77 | 78 | - _`my_first_image` the name of our image whose container we want to run._ 79 | 80 | **Great! our application is dockerized.** You will see the output in "hello world" in terminal. 81 | 82 | _See! how easy it is to dockerize an application._ 83 | -------------------------------------------------------------------------------- /03-python-programming/02-variables_and_arithematic_operators/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | This script demonstrates basic variable assignments and arithmetic operations in Python. 3 | It includes examples of printing variable values, performing addition, subtraction, division, 4 | multiplication, and exponentiation. 5 | """ 6 | 7 | # Assigning string value to the variable 'name' 8 | name = "Rehan" 9 | 10 | # Assigning integer value to the variable 'age' 11 | age = 21 12 | 13 | # Assigning boolean value to the variable 'isMarried' 14 | isMarried = True 15 | 16 | # Printing the values of the variables 17 | print(name) 18 | print(age) 19 | print(isMarried) 20 | 21 | # The following block of code is commented out. 22 | # It takes two numbers as input from the user, but is currently inactive. 23 | 24 | # first_number = int(input("Enter first number: ")) 25 | # # first_number = int(first_number) 26 | # second_number = int(input("Enter second number: ")) 27 | 28 | # Assigning predefined values to the variables for arithmetic operations 29 | first_number = 5 30 | second_number = 10 31 | 32 | # Calculating the sum of 'first_number' and 'second_number' 33 | sum = first_number + second_number 34 | # Printing the result of the addition 35 | print("Sum is: ", sum) 36 | 37 | # Reassigning new values to the variables for further arithmetic operations 38 | first_number = 50 39 | second_number = 24 40 | 41 | # Calculating the difference between 'first_number' and 'second_number' 42 | sub = first_number - second_number 43 | # Printing the result of the subtraction 44 | print("Sub is: ", sub) 45 | 46 | # Calculating the division of 'first_number' by 'second_number' 47 | div = first_number / second_number 48 | 49 | # Calculating the multiplication of 'first_number' and 'second_number' 50 | mul = first_number * second_number 51 | 52 | # Calculating the exponentiation of 'first_number' to the power of 'second_number' 53 | pow = first_number ** second_number 54 | 55 | 56 | # The results of the division, multiplication, and exponentiation are calculated but not printed. 57 | -------------------------------------------------------------------------------- /03-python-programming/02-variables_and_arithematic_operators/readme.md: -------------------------------------------------------------------------------- 1 | # CLASS-03 PART-02 2 | 3 | ## Variables in Python 4 | 5 | We have learnt different data types in Python. When we write a python program, we have to provide data in it. But we have to declare variables to store the data in it. 6 | 7 | **Let's take a real world example which will help us to understand variables in python.** 8 | 9 | We all have seen libraries where hundreds and thousands of books are available. If we want to read any book, we have to search for it. To search a book, we take help of the labels. Imagine if books/racks are not labeled, how we can search a particular book among hundred of thousands of book in the library. It will take a lot of time. 10 | 11 | Just like labels help us to identify specific books, variables help us to store and identify specific data in our Python programs. For example, we can use a variable named `student_name` to store the name of a students, `quarter` to current quarter of the student and `student_emails` to store the emails of the students. 12 | 13 | ```python 14 | student_name = "Musa Abdullah" 15 | quarter = "q1" 16 | student_email = "musa@example.com"] 17 | ``` 18 | 19 | By using these variables, we can easily manipulate and access the student information without remembering the actual data. This makes our Python programs more organized, efficient, and easier to understand. 20 | 21 | **So what we understood is that** 22 | 23 | - A variable is like a label that points to a specific location in the computer's memory where data is stored. 24 | - Variables are named storage locations for data. 25 | - They make code more readable and maintainable. 26 | - The data stored in a variable can be changed. 27 | 28 | ## Operators in Python 29 | 30 | ### 1. Arithematic Operators 31 | 32 | 1- `+` Adding two numbers, or joining two strings 33 | 34 | 2- `-` Subtracting two numbers 35 | 36 | 3- `*` Multiplying two numbers 37 | 38 | 4- `/` Dividing two numbers 39 | 40 | 5- `**` Power of two numbers 41 | 42 | 6- `%` Modulus of two numbers 43 | 44 | 7- `//` Floor division of two numbers 45 | 46 | \*For assignment "Create a Calculator" that takes two numbers as input, use this docker command to run the container `docker run -it your_image_name` 47 | -------------------------------------------------------------------------------- /03-python-programming/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usmanashrf/batch63-cloud-native-modern-python-classwork/7e44bb72d1ee0d7b263780ed67ca3d2564b06777/03-python-programming/image.png -------------------------------------------------------------------------------- /03-python-programming/readme.md: -------------------------------------------------------------------------------- 1 | # 1. Introduction to Python 2 | 3 | Python is an open-source, general-purpose, high-level, interpreted, object-oriented, dynamic programming language. 4 | 5 | ## 1.1 Where we use Python? 6 | 7 | [Python Applications](https://wiki.python.org/moin/Applications) 8 | 9 | - Web and Internet development (e.g., Django and Pyramid frameworks, Flask and Bottle micro-frameworks). 10 | - Scientific and numeric computing (e.g., SciPy - a collection of packages for the purposes of mathematics, science, and engineering 11 | - Ipython - an interactive shell that features editing and recording of work sessions). 12 | - Desktop GUIs (e.g., wxWidgets, Kivy, Qt) 13 | - Software Development (build control, management, and testing - Scons, Buildbot, Apache Gump, Roundup, Trac) 14 | 15 | * Business applications (ERP and e-commerce systems - Odoo, Tryton) 16 | (Source: https://www.python.org/about/apps)\* 17 | 18 | ## 1.2 What is Machine Language 19 | 20 | Machine language is the language understood by a computer. It is very difficult to understand, but it is the only thing that the computer can work with. All programs and programming languages eventually generate or run programs in machine language. Machine language is made up of instructions and data that are all binary numbers. Machine language is normally displayed in hexadecimal form so that it is a little bit easier to read. Assembly language is almost the same as machine language, except that the instructions, variables and addresses have names instead of just hex numbers. 21 | 22 | _Source: [MICHAEL L. SCHMIT, in Pentium™ Processor, 1995](https://www.sciencedirect.com/topics/engineering/machine-language#:~:text=Machine%20language%20is%20made%20up,little%20bit%20easier%20to%20read.)_ 23 | 24 | ## 1.3 Python, a high-level programming language 25 | 26 | - Computer (machine) understands only one language i.e. Machine Language. 27 | - Machine Language is not humand readable (0s and 1s) 28 | - A language either natural or programming has 29 | 30 | - alphabets: a, b, c, d, e. 31 | - lexis: dictionary. 32 | - syntax: rules (like grammar in english). 33 | - semantics: should make sense. 34 | 35 | - A language that is simple and easy to understand. 36 | - A language simpler than natural language. 37 | - More complex than Machine Language. 38 | - A program (set of instructions) written in high-level language is called a **source code**. 39 | - The file containing that code is called a **source file** 40 | 41 | ## 1.4 How computer will understand python? 42 | 43 | Computer doesn't know any language other than machine language. How we can make it understand a high-level language? 44 | We translate the source code written in high-level language into machine language. 45 | 46 | There are two main ways to convert a program from a high-level language to machine language: 47 | 48 | 1. **Compilation**: The source code is translated into machine code once, producing a file (e.g., an .exe file for Windows). This process needs to be repeated if you modify the source code. Once compiled, you can distribute the machine code file globally. The tool that does this translation is called a compiler. 49 | 50 | 2. **Interpretation**: The source code is translated into machine code every time it is run. The tool that performs this task is called an interpreter. This means you need to provide the source code and the interpreter to the end-user since the code is interpreted each time it is executed. 51 | 52 | ![How Python Works](image.png) 53 | 54 | ## 1.5 Setting up Coding Environment 55 | 56 | 1. **Python Installation**: [Download](https://www.python.org/downloads/) and install Python. 57 | 2. [Download](https://code.visualstudio.com/download) and install Visual Studio Code (an IDE). 58 | 59 | # 2. Data Types in Python 60 | 61 | ## 2.1 Data Types 62 | 63 | Those kinds of data can be stored in the computer's memory. In Python, primitive types are the most basic data types built into the language. 64 | 65 | - String -> `str` 66 | - Integer -> `int` 67 | - Float -> `float` 68 | - Complex -> `complex` 69 | - Boolean -> `bool` 70 | - None -> `None` 71 | 72 | ```python 73 | # Integer 74 | integer_value = 42 75 | print("Integer:", integer_value) 76 | 77 | # Float 78 | float_value = 3.14 79 | print("Float:", float_value) 80 | 81 | # Complex 82 | complex_value = 1 + 2j 83 | print("Complex:", complex_value) 84 | 85 | # String 86 | string_value = "Hello, World!" 87 | print("String:", string_value) 88 | 89 | # Boolean 90 | boolean_true = True 91 | boolean_false = False 92 | print("Boolean True:", boolean_true) 93 | print("Boolean False:", boolean_false) 94 | 95 | # NoneType 96 | none_value = None 97 | print("NoneType:", none_value) 98 | ``` 99 | 100 | ### 2.1.1 Python Literals 101 | 102 | A literal is data whose values are determined by the literal itself. 103 | In Python, literals are notations for constant values of a particular type. They represent fixed values in the code. Python supports several types of literals, including numeric, string, boolean, and special literals. 104 | 105 | ```python 106 | # Numeric Literals 107 | integer_literal = 42 108 | float_literal = 3.14 109 | complex_literal = 1 + 2j 110 | 111 | # String Literals 112 | single_quote_string = 'Hello' 113 | double_quote_string = "World" 114 | triple_quote_string = '''This is a 115 | multi-line string''' 116 | 117 | # Boolean Literals 118 | true_literal = True 119 | false_literal = False 120 | 121 | # Special Literal 122 | none_literal = None 123 | 124 | # Printing the literals 125 | print("Integer Literal:", integer_literal) 126 | print("Float Literal:", float_literal) 127 | print("Complex Literal:", complex_literal) 128 | print("Single Quote String:", single_quote_string) 129 | print("Double Quote String:", double_quote_string) 130 | print("Triple Quote String:", triple_quote_string) 131 | print("Boolean Literal True:", true_literal) 132 | print("Boolean Literal False:", false_literal) 133 | print("Special Literal None:", none_literal) 134 | ``` 135 | 136 | ## 2.2. Collection Data Types in Python 137 | 138 | ### 2.2.1 List 139 | 140 | An ordered, mutable collection of items. 141 | 142 | ```python 143 | names_list: list[str] = ["Rehan", "Musa", "Abdullah"] 144 | numbered_list: list[int] = [1, 2, 3, 4, 5] 145 | mixed_list: list[int | str | float | bool] = [1, "Rehan", 3.14, True] 146 | ``` 147 | 148 | ### 2.2.2 Tuple 149 | 150 | An ordered, immutable collection of items. 151 | 152 | ```python 153 | names_tuple: tuple[str] = ("Rehan", "Musa", "Abdullah") 154 | numbered_tuple: tuple[int] = (1, 2, 3, 4, 5) 155 | mixed_tuple: tuple[int | str | float | bool] = (1, "Rehan", 3.14, True) 156 | ``` 157 | 158 | ### 2.2.3 Set 159 | 160 | An unordered collection of unique items. 161 | 162 | ```python 163 | names_set: set[str] = {"Rehan", "Musa", "Abdullah"} 164 | numbered_set: set[int] = {1, 2, 3, 4, 5} 165 | mixed_set: set[int | str | float | bool] = {1, "Rehan", 3.14, True} 166 | ``` 167 | 168 | ### 2.2.4 Dictionary 169 | 170 | An unordered collection of key-value pairs. 171 | 172 | ```python 173 | names_dict: dict[str, str|int] = {"name": "Rehan", "age": "22"} 174 | numbered_dict: dict[int, str] = {1: "Rehan", 2: "Musa", 3: "Abdullah"} 175 | ``` 176 | 177 | ### 2.3. Specialized Data Types 178 | 179 | #### 2.3.1 Range 180 | 181 | Represents an immutable sequence of numbers. 182 | 183 | ```python 184 | range_object: range = range(1, 10) 185 | ``` 186 | 187 | #### 2.3.2 Bytes 188 | 189 | ```python 190 | byte_string: bytes = b"Hello, World!" 191 | ``` 192 | 193 | - Bytes 194 | - Bytearray 195 | - Frozen set 196 | - Generator 197 | - Ellipsis 198 | - Class 199 | - Type 200 | - Ellipsis 201 | -------------------------------------------------------------------------------- /04-class_work/04a-pep8_guidelines/readme.md: -------------------------------------------------------------------------------- 1 | # PEP 8 Guidelines 2 | 3 | ## A- PEP 8 Variable Naming Conventions 4 | 5 | PEP 8 provides several guidelines for naming variables, which help maintain consistency and readability in Python code. Here are the main variable naming conventions as specified by PEP 8: 6 | 7 | ### 1. **Descriptive Naming** 8 | 9 | - Variable names should be descriptive and meaningful. 10 | 11 | ```python 12 | count = 0 13 | total_price = 100.50 14 | ``` 15 | 16 | ### 2. Lowercase with Underscores 17 | 18 | - Variable names should be in lowercase and words should be separated by underscores. 19 | 20 | ```python 21 | user_name = "JohnDoe" 22 | item_count = 42 23 | ``` 24 | 25 | ### 3. Avoid Single Character Names 26 | 27 | - Avoid using single character names except for loop counters or in very short blocks. 28 | 29 | ````python 30 | for i in range(10): 31 | print(i) 32 | ``` 33 | ### 4. Constants in All Capitals 34 | - Constants should be written in all capital letters with underscores separating words. 35 | ```python 36 | MAX_CONNECTIONS = 100 37 | PI = 3.14159 38 | ```` 39 | 40 | ### 5. Class Names 41 | 42 | - Class names should follow the CapWords convention (also known as PascalCase). 43 | 44 | ```python 45 | class MyClass: 46 | pass 47 | 48 | 49 | class EmployeeRecord: 50 | pass 51 | ``` 52 | 53 | ### 6. Instance and Class Variables 54 | 55 | - Instance variables should follow the same lowercase with underscores convention. 56 | - Class variables should be capitalized if they are constants. 57 | 58 | ```python 59 | class ExampleClass: 60 | class_variable = 0 61 | 62 | def __init__(self, value): 63 | self.instance_variable = value 64 | ``` 65 | 66 | ### 7. Function and Method Names 67 | 68 | - Function and method names should be lowercase, with words separated by underscores as necessary to improve readability. 69 | 70 | ```python 71 | def my_function(): 72 | pass 73 | 74 | class MyClass: 75 | def my_method(self): 76 | pass 77 | ``` 78 | 79 | ### 8. Private Variables 80 | 81 | - Use a leading underscore to indicate a private variable. 82 | 83 | ```python 84 | _private_variable = 42 85 | 86 | class MyClass: 87 | def __init__(self): 88 | self._private_instance_variable = "private" 89 | 90 | ``` 91 | 92 | ### 9. Avoid Trailing Underscores 93 | 94 | - Avoid using trailing underscores in variable names. 95 | 96 | ```python 97 | # Use class_ instead of class which is a keyword 98 | class_ = "Math" 99 | 100 | ``` 101 | 102 | ### 10. Double Leading Underscores 103 | 104 | - Use double leading underscores to invoke name mangling. 105 | 106 | ```python 107 | class MyClass: 108 | def __init__(self): 109 | self.__mangled_name = "mangled" 110 | ``` 111 | 112 | ### 11. Variables with Special Meanings 113 | 114 | - Follow the naming conventions for variables with special meanings (e.g., **all**, **author**, **version**). 115 | 116 | ```python 117 | __all__ = ['module1', 'module2'] 118 | __version__ = '1.0' 119 | __author__ = 'Your Name' 120 | 121 | ``` 122 | 123 | ## B- Indentation and Line Length 124 | 125 | 1. **Indentation** 126 | 127 | - Use 4 spaces per indentation level. 128 | ```python 129 | def example_function(): 130 | a = 5 131 | b = 10 132 | return a + b 133 | ``` 134 | ```python 135 | if True: 136 | print("Hello, World!") 137 | ``` 138 | 139 | 2. **Maximum Line Length** 140 | 141 | - Limit all lines to a maximum of 79 characters. 142 | ```python 143 | def example_function_with_long_name(argument_one, argument_two, argument_three): 144 | return argument_one + argument_two + argument_three 145 | ``` 146 | ```python 147 | long_variable_name = "This is a very long string that goes beyond the limit of 79 characters." 148 | ``` 149 | 150 | 3. **Blank Lines** 151 | 152 | - Surround top-level function and class definitions with two blank lines. 153 | - Method definitions inside a class are surrounded by a single blank line. 154 | 155 | ```python 156 | class MyClass: 157 | 158 | def method_one(self): 159 | pass 160 | 161 | def method_two(self): 162 | pass 163 | ``` 164 | 165 | ```python 166 | def function_one(): 167 | pass 168 | 169 | 170 | def function_two(): 171 | pass 172 | ``` 173 | 174 | ## C- Imports 175 | 176 | 4. **Imports** 177 | - Imports should usually be on separate lines. 178 | - Imports should be grouped in the following order: standard library imports, related third-party imports, local application/library-specific imports. 179 | ```python 180 | import os 181 | import sys 182 | ``` 183 | ```python 184 | from subprocess import Popen, PIPE 185 | from mymodule import my_function 186 | ``` 187 | 188 | ## D- String Quotes 189 | 190 | 5. **String Quotes** 191 | - In a Python program, pick a rule and stick to it. Single or double quotes are acceptable. 192 | ```python 193 | my_string = "Hello, World!" 194 | another_string = 'Hello, World!' 195 | ``` 196 | 197 | ## E- Whitespace 198 | 199 | 6. **Whitespace in Expressions and Statements** 200 | 201 | - Avoid extraneous whitespace in the following situations. 202 | 203 | ```python 204 | # Correct: 205 | x = 1 206 | y = 2 207 | long_variable = 3 208 | 209 | # Incorrect: 210 | x = 1 211 | y = 2 212 | long_variable = 3 213 | ``` 214 | 215 | ## F- Comments 216 | 217 | 7. **Comments** 218 | 219 | - Comments should be complete sentences. Use a capital letter to start the comment, and end it with a period. 220 | 221 | ```python 222 | # This is a correct comment. 223 | a = 5 # This is an inline comment. 224 | 225 | # incorrect comments. 226 | a = 5 # this is an incorrect comment 227 | ``` 228 | 229 | ## G- Naming Conventions 230 | 231 | 8. **Naming Conventions** 232 | 233 | - Follow standard naming conventions: function names, variable names, and filenames should be lowercase with words separated by underscores as necessary to improve readability. 234 | 235 | ```python 236 | def my_function(): 237 | pass 238 | 239 | variable_name = 10 240 | ``` 241 | 242 | ## H- Programming Recommendations 243 | 244 | 9. **Programming Recommendations** 245 | 246 | - Always use `def` for creating functions, not `lambda` for assignments. 247 | 248 | ```python 249 | def add(x, y): 250 | return x + y 251 | 252 | # Instead of: 253 | add = lambda x, y: x + y 254 | ``` 255 | 256 | 10. **Module Level Dunder Names** 257 | 258 | - Module level "dunders" (i.e. names with two leading and two trailing underscores) such as `__all__`, `__author__`, `__version__`, etc. should be placed after the module docstring and before any import statements except `from __future__` imports. 259 | 260 | ```python 261 | """Module docstring.""" 262 | 263 | __all__ = ['foo', 'bar'] 264 | __version__ = '1.0' 265 | __author__ = 'Your Name' 266 | 267 | import os 268 | import sys 269 | ``` 270 | 271 | 11. **Top-level Script Environment** 272 | 273 | - Always use `if __name__ == "__main__":` construct for top-level script environment. 274 | 275 | ```python 276 | def main(): 277 | print("Hello, World!") 278 | 279 | if __name__ == "__main__": 280 | main() 281 | ``` 282 | 283 | 12. **Trailing Commas** 284 | 285 | - Avoid using trailing commas in a list. 286 | 287 | ```python 288 | my_list = [ 289 | 1, 290 | 2, 291 | 3, 292 | ] 293 | 294 | # Correct: 295 | my_list = [1, 2, 3] 296 | ``` 297 | 298 | 13. **Method Definitions** 299 | 300 | - Always use `self` as the first argument in instance methods. 301 | ```python 302 | class MyClass: 303 | def method(self): 304 | pass 305 | ``` 306 | 307 | ## I-Line Breaks and Statements 308 | 309 | 14. **Line Breaks** 310 | 311 | - Use backslashes for line continuation if needed. 312 | ```python 313 | total = item_one + item_two + item_three + \ 314 | item_four + item_five 315 | ``` 316 | 317 | 15. **Compound Statements** 318 | 319 | - Avoid using compound statements (multiple statements on the same line). 320 | 321 | ```python 322 | # Correct: 323 | if foo == 'bar': 324 | do_something() 325 | 326 | # Incorrect: 327 | if foo == 'bar': do_something() 328 | ``` 329 | 330 | ## J- Accessing Names 331 | 332 | 16. **Accessing Names in a Module** 333 | 334 | - Use absolute imports whenever possible. 335 | ```python 336 | import mypkg.sibling 337 | from mypkg import sibling 338 | from mypkg.sibling import example_function 339 | ``` 340 | -------------------------------------------------------------------------------- /04-class_work/04b-string_methods/readme.md: -------------------------------------------------------------------------------- 1 | ## Python String Methods with Examples 2 | 3 | ### Basic String Methods 4 | 5 | **1. capitalize()** 6 | Capitalizes the first character of the string. 7 | ```python 8 | text = "hello, world!" 9 | capitalized_text = text.capitalize() 10 | print(capitalized_text) # Output: Hello, world! 11 | ``` 12 | 13 | **2. casefold()** 14 | Converts the string to lowercase for caseless comparisons. 15 | ```python 16 | text = "HELLO WORLD" 17 | casefolded_text = text.casefold() 18 | print(casefolded_text) # Output: hello world 19 | ``` 20 | 21 | **3. center()** 22 | Returns a centered string of a specified length. 23 | ```python 24 | text = "hello" 25 | centered_text = text.center(10, "*") 26 | print(centered_text) # Output: ***hello*** 27 | ``` 28 | 29 | **4. count()** 30 | Returns the number of occurrences of a substring. 31 | ```python 32 | text = "banana" 33 | count_a = text.count("a") 34 | print(count_a) # Output: 3 35 | ``` 36 | 37 | **5. encode()** 38 | Encodes the string into a bytes object using a specified encoding. 39 | ```python 40 | text = "hello" 41 | encoded_text = text.encode("utf-8") 42 | print(encoded_text) # Output: b'hello' 43 | ``` 44 | 45 | **6. endswith()** 46 | Checks if the string ends with a specified suffix. 47 | ```python 48 | text = "hello, world!" 49 | ends_with_world = text.endswith("world!") 50 | print(ends_with_world) # Output: True 51 | ``` 52 | 53 | **7. expandtabs()** 54 | Replaces tabs with spaces. 55 | ```python 56 | text = "hello\tworld" 57 | expanded_text = text.expandtabs(4) 58 | print(expanded_text) # Output: hello world 59 | ``` 60 | 61 | **8. find()** 62 | Returns the index of the first occurrence of a substring. 63 | ```python 64 | text = "hello, world!" 65 | index = text.find("world") 66 | print(index) # Output: 7 67 | ``` 68 | 69 | **9. format()** 70 | Formats a string using placeholders. 71 | ```python 72 | name = "Alice" 73 | age = 30 74 | formatted_string = "Hello, my name is {} and I am {} years old.".format(name, age) 75 | print(formatted_string) # Output: Hello, my name is Alice and I am 30 years old. 76 | ``` 77 | 78 | **10. format_map()** 79 | Formats a string using a mapping. 80 | ```python 81 | values = {'name': 'Alice', 'age': 30} 82 | formatted_string = "Hello, my name is {name} and I am {age} years old.".format_map(values) 83 | print(formatted_string) # Output: Hello, my name is Alice and I am 30 years old. 84 | ``` 85 | 86 | **11. index()** 87 | Returns the index of the first occurrence of a substring, raising a ValueError if not found. 88 | ```python 89 | text = "hello, world!" 90 | index = text.index("world") 91 | print(index) # Output: 7 92 | ``` 93 | 94 | **12. isalnum()** 95 | Returns True if all characters are alphanumeric. 96 | ```python 97 | text = "hello123" 98 | is_alnum = text.isalnum() 99 | print(is_alnum) # Output: True 100 | ``` 101 | 102 | **13. isalpha()** 103 | Returns True if all characters are alphabetic. 104 | ```python 105 | text = "hello" 106 | is_alpha = text.isalpha() 107 | print(is_alpha) # Output: True 108 | ``` 109 | 110 | **14. isascii()** 111 | Returns True if all characters are ASCII. 112 | ```python 113 | text = "hello" 114 | is_ascii = text.isascii() 115 | print(is_ascii) # Output: True 116 | ``` 117 | 118 | **15. isdecimal()** 119 | Returns True if all characters are decimal digits. 120 | ```python 121 | text = "123" 122 | is_decimal = text.isdecimal() 123 | print(is_decimal) # Output: True 124 | ``` 125 | 126 | **16. isdigit()** 127 | Returns True if all characters are digits. 128 | ```python 129 | text = "123" 130 | is_digit = text.isdigit() 131 | print(is_digit) # Output: True 132 | ``` 133 | 134 | **17. isidentifier()** 135 | Returns True if the string is a valid identifier. 136 | ```python 137 | text = "my_variable" 138 | is_identifier = text.isidentifier() 139 | print(is_identifier) # Output: True 140 | ``` 141 | 142 | **18. islower()** 143 | Returns True if all characters are lowercase. 144 | ```python 145 | text = "hello" 146 | is_lower = text.islower() 147 | print(is_lower) # Output: True 148 | ``` 149 | 150 | **19. isnumeric()** 151 | Returns True if all characters are numeric. 152 | ```python 153 | text = "123" 154 | is_numeric = text.isnumeric() 155 | print(is_numeric) # Output: True 156 | ``` 157 | 158 | **20. isprintable()** 159 | Returns True if all characters are printable. 160 | ```python 161 | text = "hello world" 162 | is_printable = text.isprintable() 163 | print(is_printable) # Output: True 164 | ``` 165 | 166 | **21. isspace()** 167 | Returns True if all characters are whitespace. 168 | ```python 169 | text = " \t\n" 170 | is_space = text.isspace() 171 | print(is_space) # Output: True 172 | ``` 173 | 174 | **22. istitle()** 175 | Returns True if the string is a titlecased string. 176 | ```python 177 | text = "Hello, World!" 178 | is_title = text.istitle() 179 | print(is_title) # Output: True 180 | ``` 181 | 182 | **23. isupper()** 183 | Returns True if all characters are uppercase. 184 | ```python 185 | text = "HELLO" 186 | is_upper = text.isupper() 187 | print(is_upper) # Output: True 188 | ``` 189 | 190 | **24. join()** 191 | Concatenates elements of an iterable into a string. 192 | ```python 193 | words = ["hello", "world"] 194 | text = " ".join(words) 195 | print(text) # Output: hello world 196 | ``` 197 | 198 | **25. ljust()** 199 | Returns a left-justified string of a specified length. 200 | ```python 201 | text = "hello" 202 | left_justified = text.ljust(10, "*") 203 | print(left_justified) # Output: hello***** 204 | ``` 205 | 206 | **26. lower()** 207 | Converts all characters to lowercase. 208 | ```python 209 | text = "HELLO" 210 | lower_text = text.lower() 211 | print(lower_text) # Output: hello 212 | ``` 213 | 214 | **27. lstrip()** 215 | Removes leading whitespace from the string. 216 | ```python 217 | text = " hello, world! " 218 | stripped_text = text.lstrip() 219 | print(stripped_text) # Output: hello, world! 220 | ``` 221 | 222 | **28. maketrans()** 223 | Creates a translation table for use with the translate() method. 224 | ```python 225 | translation_table = str.maketrans("aeiou", "12345") 226 | text = "hello" 227 | translated_text = text.translate(translation_table) 228 | print(translated_text) # Output: h1ll0 229 | ``` 230 | 231 | **29. partition()** 232 | Splits the string into a tuple of three parts based on the first occurrence of a separator. 233 | ```python 234 | text = "hello, world!" 235 | parts = text.partition(", ") 236 | print(parts) # Output: ('hello', ', ', 'world!') 237 | ``` 238 | 239 | **30. removeprefix()** 240 | Removes a prefix from the string if it exists. 241 | ```python 242 | text = "hello world" 243 | removed_prefix = text.removeprefix("hello ") 244 | print(removed_prefix) # Output: world 245 | ``` 246 | 247 | **31. removesuffix()** 248 | Removes a suffix from the string if it exists. 249 | ```python 250 | text = "hello world!" 251 | removed_suffix = text.removesuffix(" world!") 252 | print(removed_suffix) # Output: hello 253 | ``` 254 | 255 | **32. replace()** 256 | Replaces occurrences of a substring with another substring. 257 | ```python 258 | text = "hello, world!" 259 | replaced_text = text.replace("world", "Python") 260 | print(replaced_text) # Output: Hello, Python! 261 | ``` 262 | 263 | **33. rfind()** 264 | Returns the index of the last occurrence of a substring. 265 | ```python 266 | text = "hello, world, hello" 267 | index = text.rfind("hello") 268 | print(index) # Output: 13 269 | ``` 270 | 271 | **34. rindex()** 272 | Returns the index of the last occurrence of a substring, raising a ValueError if not found. 273 | ```python 274 | text = "hello, world, hello" 275 | index = text.rindex("hello") 276 | print(index) # Output: 13 277 | ``` 278 | 279 | **35. rjust()** 280 | Returns a right-justified string of a specified length. 281 | ```python 282 | text = "hello" 283 | right_justified = -------------------------------------------------------------------------------- /04-class_work/04b-string_methods/string_methods.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "Rehan\n" 13 | ] 14 | } 15 | ], 16 | "source": [ 17 | "text = \"rehan\"\n", 18 | "capitalized_text = text.capitalize()\n", 19 | "print(capitalized_text)" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 2, 25 | "metadata": {}, 26 | "outputs": [ 27 | { 28 | "name": "stdout", 29 | "output_type": "stream", 30 | "text": [ 31 | "hello world\n" 32 | ] 33 | } 34 | ], 35 | "source": [ 36 | "text = \"HELLO WORLD\"\n", 37 | "casefolded_text = text.casefold()\n", 38 | "print(casefolded_text)" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": 3, 44 | "metadata": {}, 45 | "outputs": [ 46 | { 47 | "name": "stdout", 48 | "output_type": "stream", 49 | "text": [ 50 | "**hello***\n" 51 | ] 52 | } 53 | ], 54 | "source": [ 55 | "text = \"hello\"\n", 56 | "centered_text = text.center(10, \"*\")\n", 57 | "print(centered_text) " 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 5, 63 | "metadata": {}, 64 | "outputs": [ 65 | { 66 | "name": "stdout", 67 | "output_type": "stream", 68 | "text": [ 69 | "4\n" 70 | ] 71 | } 72 | ], 73 | "source": [ 74 | "text = \"bananaa\"\n", 75 | "count_a = text.count(\"a\")\n", 76 | "print(count_a) " 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 6, 82 | "metadata": {}, 83 | "outputs": [ 84 | { 85 | "name": "stdout", 86 | "output_type": "stream", 87 | "text": [ 88 | "True\n" 89 | ] 90 | } 91 | ], 92 | "source": [ 93 | "text = \"hello, world!\"\n", 94 | "ends_with_world = text.endswith(\"world!\")\n", 95 | "print(ends_with_world) " 96 | ] 97 | }, 98 | { 99 | "cell_type": "code", 100 | "execution_count": null, 101 | "metadata": {}, 102 | "outputs": [], 103 | "source": [ 104 | "name = \"Alice\"\n", 105 | "age = 30\n", 106 | "formatted_string = \"Hello, my name is {} and I am {} years old.\".format(name, age)\n", 107 | "print(formatted_string)" 108 | ] 109 | } 110 | ], 111 | "metadata": { 112 | "kernelspec": { 113 | "display_name": "Python 3", 114 | "language": "python", 115 | "name": "python3" 116 | }, 117 | "language_info": { 118 | "codemirror_mode": { 119 | "name": "ipython", 120 | "version": 3 121 | }, 122 | "file_extension": ".py", 123 | "mimetype": "text/x-python", 124 | "name": "python", 125 | "nbconvert_exporter": "python", 126 | "pygments_lexer": "ipython3", 127 | "version": "3.12.2" 128 | } 129 | }, 130 | "nbformat": 4, 131 | "nbformat_minor": 2 132 | } 133 | -------------------------------------------------------------------------------- /04-class_work/04c-dev_container/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile 3 | { 4 | "name": "Existing Dockerfile", 5 | "build": { 6 | // Sets the run context to one level up instead of the .devcontainer folder. 7 | "context": "..", 8 | // Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename. 9 | "dockerfile": "../Dockerfile" 10 | } 11 | 12 | // Features to add to the dev container. More info: https://containers.dev/features. 13 | // "features": {}, 14 | 15 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 16 | // "forwardPorts": [], 17 | 18 | // Uncomment the next line to run commands after the container is created. 19 | // "postCreateCommand": "cat /etc/os-release", 20 | 21 | // Configure tool-specific properties. 22 | // "customizations": {}, 23 | 24 | // Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root. 25 | // "remoteUser": "devcontainer" 26 | } 27 | -------------------------------------------------------------------------------- /04-class_work/04c-dev_container/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /04-class_work/04c-dev_container/Dockerfile: -------------------------------------------------------------------------------- 1 | # base image 2 | 3 | FROM python:3.12 4 | 5 | # setup working directory in container 6 | WORKDIR /app #layer2 7 | 8 | # copy all files to app directory 9 | COPY ./main.py /app/ 10 | 11 | # command to run on container start 12 | CMD ["python", "main.py"] 13 | -------------------------------------------------------------------------------- /04-class_work/04c-dev_container/main.py: -------------------------------------------------------------------------------- 1 | print ("hello world") 2 | 3 | print ("This code is written inside Dev Container") 4 | 5 | print ("Now we are closing our dev container") -------------------------------------------------------------------------------- /05-class/05a-type_hints/main.py: -------------------------------------------------------------------------------- 1 | name : str = "Rehan" 2 | first_number : int = 5 3 | first_float : float = 5.0 4 | is_availabe : bool = True 5 | 6 | name : str = "5" -------------------------------------------------------------------------------- /05-class/05a-type_hints/readme.md: -------------------------------------------------------------------------------- 1 | # Type Hinting in Python 2 | 3 | ## Overview 4 | 5 | Type hinting is a feature introduced in Python 3.5 that allows to indicate the expected data types of variables, function arguments, and return values. While Python is a dynamically typed language, type hints provide additional context that can help with code readability, debugging, and development in large codebases. 6 | 7 | ## Benefits of Type Hinting 8 | 9 | - **Improved Readability**: Type hints make it clear what types of values are expected and returned by functions, making the code easier to understand. 10 | - **Enhanced IDE Support**: Many IDEs and editors can use type hints to provide better autocompletion, inline documentation, and error checking. 11 | - **Static Analysis**: Tools like `mypy` can analyze code for type inconsistencies without running the program, helping to catch potential bugs early. 12 | 13 | ## Basic Usage 14 | 15 | ### Variables 16 | 17 | We can use type hints when declaring variables: 18 | 19 | ```python 20 | name: str = "Alice" 21 | age: int = 30 22 | height: float = 1.75 23 | is_active: bool = True 24 | ``` 25 | -------------------------------------------------------------------------------- /05-class/05b-more_concepts/readme.md: -------------------------------------------------------------------------------- 1 | # More in Strings 2 | 3 | ## String Operators 4 | 5 | ### 1. String Concatenation (`+`) 6 | `+` operator is used to concatenate two strings. 7 | 8 | ```python 9 | str1 = "Hello" 10 | str2 = "World" 11 | result = str1 + " " + str2 12 | print(result) # Output: "Hello World" 13 | ``` 14 | 15 | ### 2. Repetition (`*`) 16 | The `*` operator is used to repeat a string a specified number of times. 17 | ```python 18 | str1 = "Hello" 19 | result = str1 * 3 20 | print(result) # Output: "HelloHelloHello" 21 | ``` 22 | 23 | ### 3. Membership (`in`, `not in`) 24 | 25 | #### 3a. Membership Operators 26 | 27 | The `in` and `not in` operators are used to check whether a substring exists within a string. 28 | ```python 29 | str1 = "Hello World" 30 | print("Hello" in str1) # Output: True 31 | print("Python" not in str1) # Output: True 32 | ``` 33 | 34 | ### 4. String Length (`len()`) 35 | The `len()` function is used to get the length of a string. 36 | ```python 37 | str1 = "Hello World" 38 | print(len(str1)) # Output: 11 39 | ``` 40 | 41 | ### 5. Indexing (`[]`) 42 | Indexing allows to access individual characters in a string using their position. 43 | ```python 44 | str1 = "Hello" 45 | print(str1[0]) # Output: "H" 46 | print(str1[-1]) # Output: "o" 47 | ``` 48 | 49 | ### 6. Slicing (`[start:stop:step]`) 50 | Slicing allows to select a range of characters in a string. 51 | ```python 52 | str1 = "Hello World" 53 | print(str1[0:5]) # Output: "Hello" 54 | print(str1[6:11]) # Output: "World" 55 | ``` 56 | 57 | ### 7. String Comparison (`==`, `!=`, `<`, `>`, `<=`, `>=`) 58 | #### 7a. Comparison Operators 59 | Compare two values and return a Boolean result (True or False). 60 | 61 | Equal to: == 62 | Not equal to: != 63 | Greater than: > 64 | Less than: < 65 | Greater than or equal to: >= 66 | Less than or equal to: <= 67 | 68 | ```python 69 | result_equal: bool = 5 == 5 70 | result_not_equal: bool = 10 != 7 71 | result_greater_than: bool = 8 > 3 72 | result_less_than: bool = 6 < 9 73 | result_greater_equal: bool = 5 >= 5 74 | result_less_equal: bool = 3 <= 4 75 | ``` 76 | 77 | Strings can be compared using comparison operators. 78 | ```python 79 | str1 = "Hello" 80 | str2 = "World" 81 | print(str1 == str2) # Output: False 82 | print(str1 != str2) # Output: True 83 | print(str1 < str2) # Output: True (since "H" comes before "W" lexicographically) 84 | ``` 85 | 86 | ### 8. String Formatting (`%`, `.format()`, `f-strings`) 87 | Strings can be formatted using different methods. 88 | ```python 89 | name = "Alice" 90 | age = 30 91 | 92 | # Using % operator 93 | print("My name is %s and I am %d years old." % (name, age)) # Output: "My name is Alice and I am 30 years old." 94 | 95 | # Using .format() 96 | print("My name is {} and I am {} years old.".format(name, age)) # Output: "My name is Alice and I am 30 years old." 97 | 98 | # Using f-strings (Python 3.6+) 99 | print(f"My name is {name} and I am {age} years old.") # Output: "My name is Alice and I am 30 years old." 100 | ``` 101 | #### 8a. Common format specifiers 102 | 1. `%s` (String Format Specifier): 103 | 104 | * `%`s is used as a placeholder for a string. It tells Python to insert the string representation of the corresponding variable into the position where `%s` appears in the string. 105 | In our example: 106 | * `%s` corresponds to the variable name, which has the value "Alice". 107 | * "My name is `%s`" becomes "My name is Alice". 108 | 2. `%d` (Integer Format Specifier): 109 | 110 | * `%d` is used as a placeholder for an integer. It tells Python to insert the integer value of the corresponding variable into the position where `%d` appears in the string. 111 | In our example: 112 | * `%d` corresponds to the variable age, which has the value 30. 113 | * "I am `%d` years old" becomes "I am 30 years old". 114 | 3. `%f` (Float Format Specifier) 115 | ```python 116 | pi = 3.14159 117 | formatted_string = "The value of pi is approximately %.2f" % pi 118 | print(formatted_string) # Output: "The value of pi is approximately 3.14" 119 | ``` 120 | 121 | 4. `%x` (Integers in Hexadecimal Format Specifier) 122 | ```python 123 | number = 255 124 | formatted_string = "The hexadecimal representation of %d is %x" % (number, number) 125 | print(formatted_string) # Output: "The hexadecimal representation of 255 is ff" 126 | ``` 127 | 128 | 5. `%o` (Integers in Octal Format Specifier) 129 | 130 | ```python 131 | number = 255 132 | formatted_string = "The octal representation of %d is %o" % (number, number) 133 | print(formatted_string) # Output: "The octal representation of 255 is 377" 134 | ``` 135 | 136 | 6. `%e` (Scientific Format Specifier) e.g `2.5e+02` 137 | ```python 138 | large_number = 25000 139 | formatted_string = "The scientific notation of %d is %e" % (large_number, large_number) 140 | print(formatted_string) # Output: "The scientific notation of 25000 is 2.500000e+04" 141 | ``` 142 | 143 | 7. `%c` (Character Format Specifier) 144 | 145 | ```python 146 | char_code = 65 147 | formatted_string = "The character for ASCII code %d is %c" % (char_code, char_code) 148 | print(formatted_string) # Output: "The character for ASCII code 65 is A" 149 | ``` 150 | 8. `%r` (Raw Format Specifier) 151 | ```python 152 | text = "Hello\nWorld" 153 | formatted_string = "Raw representation: %r" % text 154 | print(formatted_string) # Output: "Raw representation: 'Hello\\nWorld'" 155 | ``` 156 | ### 9. Escape Sequence 157 | - ```\ ``` -> Escape character 158 | - ```\n ``` -> Escape sequence 159 | - ```\n ``` -> inserts new line 160 | - ```\\ ``` -> inserts backslach 161 | - ```\' ``` -> inserts single quote 162 | - ```\" ``` -> inserts double quote 163 | - ```\t ``` -> inserts tab 164 | - ```\r ``` -> moves the cursor to the start of the line 165 | - ```\a ``` -> Beeps 166 | - ```\b ``` -> Backspace 167 | 168 | ```python 169 | print("Python Programming in \"PIAIC\"") 170 | print("Python Programming in \\PIAIC") 171 | print("Python Programming in \nPIAIC") 172 | print("Python Programming in \tPIAIC") 173 | print("Python Programming in PIAIC\rpiaic") 174 | print("Python Programming in a\bPIAIC") 175 | print("Python Programming in \aPIAIC") 176 | ``` 177 | 178 | ### 10. Assignment Operators: 179 | 180 | Assign values to variables and perform operations in a concise way. 181 | 182 | Assignment: = 183 | Addition assignment: += 184 | Subtraction assignment: -= 185 | Multiplication assignment: *= 186 | Division assignment: /= 187 | Modulus assignment: %= 188 | Floor division assignment: //= 189 | Exponentiation assignment: **= 190 | 191 | ```python 192 | x: int = 5 193 | x += 3 # Equivalent to x = x + 3 194 | y: float = 10 195 | y /= 2 # Equivalent to y = y / 2 196 | ``` 197 | 198 | ### 11. Comments. 199 | 200 | In Python, comments are lines of text in your code that are not executed as part of the program. They are meant for human readers to understand the code better. Python supports two types of comments: single-line comments and multi-line comments. 201 | 202 | #### 1. Single-line Comment 203 | 204 | Single-line comments start with the # symbol and continue to the end of the line. Everything after # on that line is considered a comment and is ignored by the Python interpreter. 205 | 206 | 207 | ```python 208 | # This is a single-line comment 209 | 210 | print("Hello, World!") # This is also a comment 211 | 212 | Provides a brief comment on a single line. 213 | ``` 214 | 215 | #### 2. Multi-line Comments: 216 | 217 | While Python doesn't have a built-in syntax for multi-line comments, you can use triple-quotes (''' or """) to create a multi-line string, and it's often used as a way to add multi-line comments. Although it's not ignored like a traditional comment, it serves the purpose of commenting out multiple lines. 218 | 219 | ```python 220 | ''' 221 | This is a multi-line comment 222 | spanning multiple lines. 223 | ''' 224 | print("Hello, World!") 225 | 226 | Demonstrates a comment spanning multiple lines. 227 | ``` 228 | 229 | ### 12. Type conversion 230 | This is the process of changing the data type of a value. Python provides functions like int(), float(), and complex() for type conversion. 231 | 232 | 233 | ```python 234 | float_value: float = 3.75 235 | int_value: int = 2 236 | 237 | float_to_int: int = int(float_value) 238 | int_to_float: float = float(int_value) 239 | 240 | Illustrates converting between data types. 241 | ``` 242 | 243 | ### 13. `type`, `dir`, `id` functions 244 | - `type` provides the type of a variable 245 | - `dir` provides the list of attributes and methods of a variable 246 | - `id` provides the memory address of a variable 247 | 248 | ```python 249 | w = True 250 | x = 5 251 | y = 3.0 252 | z = "Hello" 253 | 254 | print(type(w)) 255 | print(type(x)) 256 | print(type(y)) 257 | print(type(z)) 258 | 259 | print(dir(w)) 260 | print(dir(x)) 261 | print(dir(y)) 262 | print(dir(z)) 263 | 264 | print(id(w)) 265 | print(id(x)) 266 | print(id(y)) 267 | print(id(z)) 268 | ``` 269 | 270 | ### 14. Mutable and Immutable data types 271 | Mutable data types are those that can be changed after they are created. Immutable data types are those that cannot be changed after they are created. 272 | 273 | - Immutable data types: `int`, `float`, `str`, `bool` 274 | - Mutable data types: `list`, `set`, `dict` 275 | 276 | 277 | -------------------------------------------------------------------------------- /05-class/05c-lists/README.md: -------------------------------------------------------------------------------- 1 | - If you want to store multiple items, you would need to create a separate variable for each one. 2 | 3 | ``` 4 | student1 = "Rehan" 5 | student2 = "Muzhar" 6 | student3 = "Ibtisam" 7 | ``` 8 | 9 | This approach works fine if you have just a few items. But what if you have 10, 100, or even more? It quickly becomes unmanageable. 10 | 11 | - If you want to add or remove an item, you have to manually shift the data around or create new variables. 12 | 13 | ``` 14 | student4 = "Usman" 15 | ``` 16 | 17 | - Accessing an item requires you to remember the exact variable name associated with it. 18 | 19 | ``` 20 | print(student1) 21 | print(student2) 22 | ``` 23 | 24 | # What is List? 25 | 26 | A list in Python is like a container that can hold multiple items, one after another. Imagine you have a shopping list where you write down everything you need to buy. A Python list is very similar, but instead of just groceries, it can hold all kinds of things like numbers, words, or even other lists! 27 | 28 | ### Store Multiple Items Together: 29 | 30 | A list allows you to keep multiple items in one place. For example, if you want to keep track of all your students' names, you can store them in a list. 31 | 32 | ``` 33 | students = ["Rehan", "Muzhar", "Ibtisam"] 34 | ``` 35 | 36 | ### Access Items by Position: 37 | 38 | Lists keep items in the order you put them in. You can access any item by telling Python where it is in the list (starting from 0). 39 | 40 | ``` 41 | print(students[0]) 42 | print(students[1]) 43 | ``` 44 | 45 | ### Change Items: 46 | 47 | You can easily change an item in the list if you need to update it. 48 | 49 | ``` 50 | students[0] = "Ahmad" 51 | ``` 52 | 53 | ### Adding or Removing elements from list 54 | 55 | ``` 56 | students = ["Rehan", "Muzhar", "Ibtisam"] 57 | 58 | # Append a new student 59 | students.append("Usman") 60 | print(students) # Output: ["Rehan", "Muzhar", "Ibtisam", "Usman"] 61 | 62 | # Pop the last student 63 | last_student = students.pop() 64 | print(last_student) # Output: "Usman" 65 | print(students) # Output: ["Rehan", "Muzhar", "Ibtisam"] 66 | 67 | # Remove a specific student by name 68 | students.remove("Rehan") 69 | print(students) # Output: ["Muzhar", "Ibtisam"] 70 | 71 | 72 | 73 | # Initial list of students 74 | students: list[str] = ["Rehan", "Muzhar", "Ibtisam"] 75 | 76 | # New students to be added 77 | new_students: list[str] = ["Usman", "Ahmed"] 78 | 79 | # Extend the original students list with the new students 80 | students.extend(new_students) 81 | 82 | ``` 83 | 84 | --- 85 | 86 | ## Indexing 87 | 88 | Indexing is how you access individual elements in a list using their position in the list. In Python, lists are zero-indexed, meaning that the first element in a list is at index 0, the second element is at index 1, and so on. 89 | 90 | ``` 91 | 0 1 2 92 | students = ["Rehan", "Muzhar", "Ibtisam"] # length-1 93 | -3 -2 -1 94 | ``` 95 | 96 | ## Slicing with Indexes 97 | 98 | ``` 99 | phrase = list("LongLivePakistan!") 100 | 101 | ``` 102 | 103 | #### Positive Indexing 104 | 105 | ``` 106 | part1 = phrase[0:4] 107 | print(part1) # Output: "Long" 108 | 109 | 110 | part2 = phrase[4:8] 111 | print(part2) # Output: "Live" 112 | 113 | 114 | part3 = phrase[8:16] 115 | print(part3) # Output: "Pakistan" 116 | 117 | ``` 118 | 119 | #### Negative Indexing 120 | 121 | ``` 122 | part4 = phrase[-9:-1] 123 | print(part4) # Output: "Pakistan" 124 | 125 | 126 | part5 = phrase[-14:-10] 127 | print(part5) # Output: "Live" 128 | 129 | 130 | part6 = phrase[:-1] 131 | print(part6) # Output: "LongLivePakistan" 132 | 133 | ``` 134 | 135 | #### Slicing with Steps 136 | 137 | ``` 138 | part7 = phrase[::2] 139 | print(part7) # Output: "LnLvPksa!" 140 | 141 | 142 | 143 | part8 = phrase[0:8:2] 144 | print(part8) # Output: "LnLv" 145 | 146 | ``` 147 | 148 | Reversing the entire string 149 | ``` 150 | reversed_phrase = phrase[::-1] 151 | print(reversed_phrase) # Output: "!natsikaPevignoL" 152 | ``` 153 | 154 | Combining Positive and Negative Indexing with Steps 155 | 156 | ``` 157 | part10 = phrase[6:-2:2] 158 | print(part10) # Output: "vekaP" 159 | ``` 160 | 161 | 162 | ## Some interesting concepts in Lists 163 | 164 | ```python 165 | 166 | zero_list = [0] * 5 167 | print(zero_list) # Output: [0, 0, 0, 0, 0] 168 | 169 | # list concatenation 170 | list1 = [1, 2, 3] 171 | list2 = [4, 5, 6] 172 | combined_list = list1 + list2 173 | print(combined_list) # Output: [1, 2, 3, 4, 5, 6] 174 | 175 | # list repetition 176 | repeated_list = list1 * 3 177 | print(repeated_list) # Output: [1, 2, 3, 1, 2, 3, 1, 2, 3] 178 | 179 | # list membership 180 | a = 1 in list1 181 | print(a) # Output: True 182 | 183 | b = 7 in list1 184 | print(b) # Output: False 185 | 186 | # list length 187 | length = len(list1) 188 | print(length) # Output: 3 189 | 190 | # list and range function 191 | new_list = list(range(10)) 192 | print(new_list) # Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 193 | 194 | message = "Hello, World!" 195 | characters = list(message) 196 | print(characters) # Output: ['H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!'] 197 | 198 | # list unpacking 199 | numbered_list = [4, 8, 6] 200 | a, b, c = numbered_list 201 | print(a) # Output: 4 202 | print(b) # Output: 8 203 | print(c) # Output: 6 204 | 205 | another_list = [1,2,3,4,5,6,7,8,9,10] 206 | x, *y, z = another_list 207 | print(x) # Output: 1 208 | print(y) # Output: [2, 3, 4, 5, 6, 7, 8] 209 | print(z) # Output: 10 210 | ``` 211 | 212 | 213 | -------------------------------------------------------------------------------- /05-class/05d-nested-lists/README.md: -------------------------------------------------------------------------------- 1 | ## Organizing Students and Their Grades 2 | 3 | Imagine you are a teacher managing a classroom. You have a list of students, and for each student, you want to keep track of their grades in three different subjects: Math, Science, and English. You decide to use a nested list where each sublist represents a student, and within each sublist, you store the student’s name and their grades for the three subjects. 4 | 5 | ``` 6 | classroom = [ 7 | ["Rehan", [85, 90, 88]], # Rehan's grades: Math, Science, English 8 | ["Muzhar", [78, 85, 82]], # Muzhar's grades: Math, Science, English 9 | ["Ibtisam", [92, 88, 91]], # Ibtisam's grades: Math, Science, English 10 | ["Usman", [60, 59, 65]] # Usman's grades: Math, Science, English 11 | ] 12 | ``` 13 | 14 | In this nested list: 15 | 16 | • The outer list represents the entire classroom, with each sublist representing a student. 17 | • Each student’s sublist contains their name and another sublist of their grades in Math, Science, and English. 18 | 19 | 20 | #### Accessing Specific Grades 21 | ***Rehan’s Science Grade:*** 22 | 23 | ``` 24 | rehan_science_grade = classroom[0][1][1] 25 | print(rehan_science_grade) # Output: 90 26 | ``` 27 | 28 | ***Usman’s English Grade:*** 29 | ``` 30 | usman_english_grade = classroom[3][1][2] 31 | print(usman_english_grade) # Output: 65 32 | ``` 33 | 34 | #### Modifying a Student’s Grades 35 | ***Update Muzhar’s Math Grade:*** 36 | ``` 37 | classroom[1][1][0] = 83 38 | print(classroom[1]) # Output: ['Muzhar', [83, 85, 82]] 39 | ``` 40 | -------------------------------------------------------------------------------- /06-control_flow/06a_falsy_values/readme.md: -------------------------------------------------------------------------------- 1 | # Falsy values in Python 2 | 3 | These values are evaluated to 'false' that's why we call it `falsy` values. 4 | 5 | ```python 6 | value_01 : None = None 7 | value_02 : int = 0 8 | value_03 : float = 0.0 9 | value_04 : str = "" 10 | Value_05 : bool = False 11 | value_06 : list[None] = [] 12 | ``` 13 | And all other empty collections and sequences are also `falsy`. -------------------------------------------------------------------------------- /06-control_flow/06b_conditional_statements/readme.md: -------------------------------------------------------------------------------- 1 | # Conditional Statements in Python 2 | 3 | ## The Problem: Making Decisions in Code 4 | 5 | Imagine we're building a simple app that recommends activities based on the weather. On a sunny day, we want to suggest going for a walk, while on a rainy day, we'd suggest staying indoors and reading a book. But what if it's cloudy or snowing? How do we make our app smart enough to handle these different scenarios? 6 | 7 | Without a way to make decisions, our app would always suggest the same thing, no matter the weather. This is where we hit our first roadblock as budding programmers: **How do we make our code choose the right path?** 8 | 9 | ## The Solution: conditional statements in Python 10 | 11 | Python, like any language, needs a way to handle different situations. This is where **conditional statements** come into play. These are the structures that allow our code to decide what to do next based on certain conditions. In Python, we use `if`, `elif`, and `else` to guide these decisions. 12 | 13 | ### The Story of Choices: `if`, `elif`, and `else` 14 | 15 | Let’s dive into the story of how Python makes decisions. 16 | 17 | 1. **The `if` Statement**: 18 | 19 | - Think of `if` as the gatekeeper. It checks a condition and decides if the code block that follows should run. 20 | - For example: If the weather is sunny, the gatekeeper will open the door to the "go for a walk" suggestion. 21 | 22 | ```python 23 | weather : str = "sunny" 24 | 25 | if weather == "sunny": 26 | print("It's a beautiful day! Go for a walk.") 27 | ``` 28 | 29 | We can also use `else` statement in above exmple. 30 | 31 | - For example: We want to specify something else is done if our condition is not met, then we use `else` statement. 32 | 33 | ```python 34 | weather : str = "sunny" 35 | 36 | if weather == "sunny": 37 | print("It's a beautiful day! Go for a walk.") 38 | else: 39 | print("Read the book") 40 | ``` 41 | 42 | **Ternary Operator (Optional):** 43 | 44 | - Python offers another way to write the `if` `else` code with less number of lines i.e. **Ternary Operator**. Let's see how can we use Ternary Operator to write the same code. 45 | 46 | ```python 47 | # We can also write the above example like so. 48 | weather : str = "sunny" 49 | 50 | if weather == "sunny": 51 | message : str = "It's a beautiful day! Go for a walk." 52 | else: 53 | message : str = "Read the Book" 54 | print(message) 55 | ``` 56 | 57 | Nothing is changed so far, we just refactored the code. Also note, we've used ternary operator yet but we made a ground for understanding ternary operator. 58 | 59 | **_Use of Ternary Operator_** 60 | 61 | ```python 62 | weather : str == "sunny" 63 | message : str = "It's a beautiful day! Go for a walk." if weather == "sunny" else "Read the book" 64 | print(message) 65 | ``` 66 | 67 | - By using ternary operator, we write the `if` `else` block in a single line. 68 | - Ealier, we wrote 5 lines of code for first example and 6 lines of code for second example. 69 | - With ternary operator, we just wrote 3 lines of code which provided the same result. 70 | 71 | 2. **The `elif` Statement**: 72 | 73 | - What if we've multiple choices, how do we make the decision then? 74 | - What if the weather isn’t sunny? That’s where `elif` (short for "else if") comes in. It's the gatekeeper’s assistant, ready to check another condition if the first one fails. 75 | - For example: If the weather is not sunny but cloudy, the assistant will suggest taking an umbrella just in case. 76 | 77 | ```python 78 | weather : str = "cloudy" 79 | 80 | if weather == "sunny": 81 | print("It's a beautiful day! Go for a walk.") 82 | elif weather == "cloudy": 83 | print("It might rain. Better take an umbrella.") 84 | ``` 85 | 86 | 3. **The `else` Statement**: 87 | 88 | - Finally, there’s `else`, the last resort. If all other conditions fail, `else` steps in to provide a default action. 89 | - For example: If the weather isn’t sunny or cloudy, and perhaps it’s raining or snowing, `else` will suggest a cozy indoor activity. 90 | 91 | ```python 92 | weather : str = "rainy" 93 | 94 | if weather == "sunny": 95 | print("It's a beautiful day! Go for a walk.") 96 | elif weather == "cloudy": 97 | print("It might rain. Better take an umbrella.") 98 | else: 99 | print("Stay indoors and read a book.") 100 | ``` 101 | 102 | ## Nested Conditional Statements 103 | 104 | Will be done in onsite class 105 | 106 | ## Why It Matters 107 | 108 | By using `if`, `elif`, and `else`, we give our program the power to make decisions just like we do in real life. This makes our code dynamic, flexible, and smart enough to handle different situations. 109 | 110 | With conditional statements, we can solve problems like: 111 | 112 | - **Customizing user experiences**: Show different messages or suggestions based on user input or external factors. 113 | - **Automating tasks**: Only perform certain actions when specific conditions are met. 114 | - **Creating complex algorithms**: Make our programs more sophisticated by handling multiple scenarios with ease. 115 | 116 | ## Examples 117 | 118 | Here are some real-world examples that demonstrate the use of `if`, `elif`, nested `if`, and multiple `if` conditions in Python. 119 | 120 | ### 1. Simple `if` Statement 121 | 122 | **Scenario**: A user wants to check if they have enough balance to make a purchase. 123 | 124 | ```python 125 | balance : int = 150 126 | price : int = 100 127 | 128 | if balance >= price: 129 | print("Purchase successful!") 130 | ``` 131 | 132 | **Explanation**: The `if` statement checks if the user's balance is greater than or equal to the price. If true, the purchase is successful. 133 | 134 | ### 2. `if`-`elif`-`else` Chain 135 | 136 | **Scenario**: A system that grades students based on their score. 137 | 138 | ```python 139 | score : int = 85 140 | 141 | if score >= 90: 142 | print("Grade: A") 143 | elif score >= 80: 144 | print("Grade: B") 145 | elif score >= 70: 146 | print("Grade: C") 147 | elif score >= 60: 148 | print("Grade: D") 149 | else: 150 | print("Grade: F") 151 | ``` 152 | 153 | **Explanation**: The program checks the score against several conditions using `if` and `elif`. If none of the conditions match, the `else` block runs, assigning an "F" grade. 154 | 155 | ### 3. Nested `if` Statement 156 | 157 | **Scenario**: A store offers a discount only to members who have also made purchases above a certain amount. 158 | 159 | ```python 160 | is_member : bool = True 161 | purchase_amount : int = 250 162 | 163 | if is_member: 164 | if purchase_amount > 200: 165 | print("You are eligible for a 10% discount!") 166 | else: 167 | print("Spend more than $200 to get a discount.") 168 | else: 169 | print("Become a member to enjoy discounts.") 170 | ``` 171 | 172 | **Explanation**: The first `if` checks if the user is a member. If true, it checks if their purchase amount exceeds $200 using a nested `if`. Based on these conditions, the program decides if the user is eligible for a discount. 173 | 174 | ### 4. Multiple `if` Statements (Independent Conditions) 175 | 176 | **Scenario**: A smart home system that checks several independent conditions to set up the house for the night. 177 | 178 | ```python 179 | lights_on : bool = True 180 | doors_locked : bool = False 181 | windows_closed : bool = True 182 | 183 | if lights_on: 184 | print("Turning off the lights.") 185 | if not doors_locked: 186 | print("Locking the doors.") 187 | if windows_closed: 188 | print("All windows are closed.") 189 | ``` 190 | 191 | **Explanation**: Each `if` statement is independent and checks a different condition. The system performs actions like turning off lights, locking doors, and checking windows, regardless of the other conditions. 192 | 193 | 194 | ### 5. Nested `if` with `elif` and `else` 195 | 196 | **Scenario**: A restaurant ordering system that checks if a user has chosen a meal and then checks for special requests. 197 | 198 | ```python 199 | meal_selected : str = "burger" 200 | add_cheese : bool = True 201 | 202 | if meal_selected == "burger": 203 | print("Burger selected.") 204 | 205 | if add_cheese: 206 | print("Adding cheese.") 207 | else: 208 | print("No cheese added.") 209 | 210 | elif meal_selected == "pizza": 211 | print("Pizza selected.") 212 | else: 213 | print("Please select a meal.") 214 | ``` 215 | 216 | **Explanation**: The system first checks if a meal is selected, then performs additional checks (like adding cheese) using nested `if` statements. If the meal isn’t a burger, it checks for other meals with `elif`. 217 | 218 | These examples illustrate how `if`, `elif`, nested `if`, and multiple independent `if` statements can be used to handle various real-world scenarios in Python. 219 | -------------------------------------------------------------------------------- /06-control_flow/06c_logical_operators/readme.md: -------------------------------------------------------------------------------- 1 | # Logical Operators in Python 2 | 3 | ## The Problem: Making Complex Decisions in Code 4 | 5 | Imagine we're developing a security system for a smart home. The system should lock the doors if it's nighttime and the user is away, or if it's daytime and the user has set the system to "vacation mode." But there's a catch: we need to ensure that the system doesn't lock the doors when the user is home, even if the other conditions are met. 6 | 7 | How do we make our code handle these multiple, interconnected conditions? Without a way to combine and evaluate these conditions simultaneously, our security system might fail to work properly, leaving the house unprotected when it should be locked, or worse, locking the user out of their own home. 8 | 9 | This brings us to a critical concept in programming: **Logical Operators**. 10 | 11 | ## The Solution: Combining Conditions with Logical Operators 12 | 13 | In Python, **Logical Operators** (`and`, `or`, `not`) are essential tools that allow we to combine multiple conditions in our code. They enable our program to make complex decisions by evaluating whether a group of conditions are true or false. 14 | 15 | ### The Story of Complex Decisions: `and`, `or`, and `not` 16 | 17 | Let's explore how Python uses logical operators to manage complex scenarios in our code. 18 | 19 | 1. **The `And` Operator**: 20 | - Imagine we need to check if two conditions are both true. The `and` operator allows we to combine these conditions so that the flow of our program continues only if both are met. 21 | - In our smart home example, we want to lock the doors only if it’s nighttime **and** the user is away. 22 | 23 | ```python 24 | is_night : bool = True 25 | is_user_away : bool = True 26 | 27 | if is_night and is_user_away: 28 | print("Lock the doors.") 29 | ``` 30 | 31 | 2. **The "Or" Operator**: 32 | - Sometimes, we want to take action if at least one of several conditions is true. The `or` operator allows our program to proceed if any of the combined conditions are met. 33 | - For instance, we want to lock the doors if it’s either nighttime and the user is away **or** if the system is in "vacation mode." 34 | 35 | ```python 36 | is_vacation_mode : bool = True 37 | 38 | if (is_night and is_user_away) or is_vacation_mode: 39 | print("Lock the doors.") 40 | ``` 41 | 42 | 3. **The "Not" Operator**: 43 | - There are situations where we need to reverse a condition. The `not` operator inverts the truth value of a condition, allowing our program to act when something is **not** true. 44 | - In our case, we don’t want to lock the doors if the user is home, regardless of the other conditions. 45 | 46 | ```python 47 | is_user_home : bool = False 48 | 49 | if (is_night and is_user_away) or is_vacation_mode and not is_user_home: 50 | print("Lock the doors.") 51 | else: 52 | print("Do not lock the doors.") 53 | ``` 54 | 55 | ## Why It Matters 56 | 57 | Logical operators are the backbone of complex decision-making in our code. They allow we to evaluate multiple conditions at once, enabling our program to handle intricate scenarios with ease. By combining conditions with `and`, `or`, and `not`, we can create powerful, flexible logic that adapts to a variety of situations. 58 | 59 | With logical operators, we can solve problems like: 60 | 61 | - **Implementing security checks**: Ensure that multiple criteria are met before taking critical actions. 62 | - **Building advanced user interfaces**: Display different content based on a combination of user settings. 63 | - **Creating sophisticated algorithms**: Make our code smarter by evaluating multiple factors simultaneously. 64 | 65 | ## Conclusion 66 | 67 | Mastering logical operators is essential for writing code that can handle complex, real-world scenarios. Whether we're working on a security system, a game, or a web application, understanding how to combine conditions with `and`, `or`, and `not` will allow we to build more intelligent, responsive programs. 68 | 69 | 70 | 71 | ## Examples 72 | 73 | 74 | ### Scenario: Finding a Life Partner 75 | 76 | Imagine you're looking for a life partner, and you have certain qualities in mind that are important to you. Specifically, you want your partner to be either **handsome** and also **well-educated**. You can use logical operators to determine if a potential partner meets these criteria. 77 | 78 | ### 1. Using the `and` Operator 79 | 80 | You want to ensure that the person is both **handsome** and **well-educated**. Both conditions must be true for the person to be considered. 81 | 82 | ```python 83 | is_handsome : bool = True 84 | is_well_educated : bool = True 85 | 86 | if is_handsome and is_well_educated: 87 | print("This person might be a great match!") 88 | else: 89 | print("Keep looking for someone with both qualities.") 90 | ``` 91 | 92 | **Explanation**: The `and` operator ensures that only if the person is both good-looking (`is_handsome`) **and** well-educated (`is_well_educated`), the program will suggest that the person might be a great match. 93 | 94 | ### 2. Using the `or` Operator 95 | 96 | Now, let's say you’re a bit more flexible, and you’re open to considering someone who either has good looks or is well-educated (or both). 97 | 98 | ```python 99 | is_handsome : bool = False 100 | is_well_educated : bool= True 101 | 102 | if is_handsome or is_well_educated: 103 | print("This person could be a good match!") 104 | else: 105 | print("Keep looking for someone who meets at least one of your criteria.") 106 | ``` 107 | 108 | **Explanation**: The `or` operator allows for more flexibility. If the person meets at least one of the criteria—either being handsome **or** well-educated—the program suggests that this person could still be a good match. 109 | 110 | ### 3. Using the `not` Operator 111 | 112 | Let’s add another layer. Suppose you want to avoid someone who is not well-educated, regardless of whether they are handsome. 113 | 114 | ```python 115 | is_handsome : bool = True 116 | is_well_educated : bool = False 117 | 118 | if is_handsome and not is_well_educated: 119 | print("This person is attractive but not well-educated.") 120 | else: 121 | print("This person is either well-educated, or both attractive and well-educated.") 122 | ``` 123 | 124 | **Explanation**: Here, the `not` operator inverts the `is_well_educated` condition. If the person is attractive but not well-educated, the program points out the lack of education. Otherwise, it suggests that the person meets at least the education criterion or both. 125 | 126 | ### 4. Combining Multiple Logical Operators 127 | 128 | Finally, let's say you have a scenario where you want to find someone who is either well-educated **and** handsome, or at least has one of these qualities. 129 | 130 | ```python 131 | is_handsome : bool = False 132 | is_well_educated : bool = True 133 | is_high_income : bool = True 134 | 135 | if (is_handsome and is_well_educated) or is_high_income: 136 | print("This person is well-educated, which is important to you.") 137 | else: 138 | print("Keep looking for someone who fits your criteria better.") 139 | ``` 140 | 141 | **Explanation**: This condition checks for either both qualities being present, or at least being well-educated. The logical operators `and`, `or`, and `not` allow you to evaluate complex conditions and make more nuanced decisions. 142 | -------------------------------------------------------------------------------- /06-control_flow/06d_membership&identity_operatros/readme.md: -------------------------------------------------------------------------------- 1 | # Membership & Identity Operators 2 | 3 | Membership and identity operators in Python are used to check the presence of an element within a sequence and to compare objects' identities, respectively. Here's an explanation of each: 4 | 5 | ## Membership Operators 6 | 7 | Membership operators are used to test whether a value or variable is found within a sequence, such as a string, list, or tuple. Python provides two membership operators: 8 | 9 | - `in` 10 | - `not in` 11 | 12 | ### 1. `in` Operator 13 | 14 | The `in` operator returns `True` if the specified value is found in the sequence. 15 | 16 | #### Example: 17 | ```python 18 | # Checking if an element is in a list 19 | fruits = ["apple", "banana", "cherry"] 20 | print("apple" in fruits) # Output: True 21 | print("orange" in fruits) # Output: False 22 | 23 | # Checking if a character is in a string 24 | message = "Hello, World!" 25 | print("H" in message) # Output: True 26 | print("h" in message) # Output: False (case-sensitive) 27 | ``` 28 | 29 | ### 2. `not in` Operator 30 | 31 | The `not in` operator returns `True` if the specified value is not found in the sequence. 32 | 33 | #### Example: 34 | ```python 35 | # Checking if an element is not in a list 36 | fruits = ["apple", "banana", "cherry"] 37 | print("orange" not in fruits) # Output: True 38 | print("banana" not in fruits) # Output: False 39 | 40 | # Checking if a substring is not in a string 41 | message = "Hello, World!" 42 | print("Python" not in message) # Output: True 43 | ``` 44 | 45 | ## Identity Operators 46 | 47 | Identity operators are used to compare the memory location of two objects. They determine whether two variables refer to the same object in memory. Python provides two identity operators: 48 | 49 | - `is` 50 | - `is not` 51 | 52 | ### 1. `is` Operator 53 | 54 | The `is` operator returns `True` if two variables point to the same object (i.e., have the same memory location). 55 | 56 | #### Example: 57 | ```python 58 | # Comparing two variables that point to the same object 59 | a = [1, 2, 3] 60 | b = a 61 | print(a is b) # Output: True 62 | 63 | # Comparing two variables that point to different objects 64 | c = [1, 2, 3] 65 | print(a is c) # Output: False 66 | ``` 67 | 68 | In the first comparison, `a` and `b` are pointing to the same list object in memory, so `a is b` returns `True`. In the second comparison, `a` and `c` point to different list objects, even though they contain the same elements, so `a is c` returns `False`. 69 | 70 | ### 2. `is not` Operator 71 | 72 | The `is not` operator returns `True` if two variables do not point to the same object (i.e., have different memory locations). 73 | 74 | #### Example: 75 | ```python 76 | # Comparing two variables that do not point to the same object 77 | a = [1, 2, 3] 78 | b = [1, 2, 3] 79 | print(a is not b) # Output: True 80 | 81 | # Comparing two variables that point to the same object 82 | c = a 83 | print(a is not c) # Output: False 84 | ``` 85 | 86 | In the first example, `a` and `b` point to different objects in memory, so `a is not b` returns `True`. In the second example, `a` and `c` point to the same object, so `a is not c` returns `False`. 87 | 88 | ## Key Differences Between `==` and `is` 89 | 90 | - `==` compares the values of two objects to see if they are equal. 91 | - `is` compares the identities of two objects to see if they refer to the same object in memory. 92 | 93 | ### Example: 94 | ```python 95 | x = [1, 2, 3] 96 | y = [1, 2, 3] 97 | 98 | print(x == y) # Output: True (values are equal) 99 | print(x is y) # Output: False (different objects in memory) 100 | ``` 101 | 102 | In this example, `x == y` returns `True` because the lists have the same elements, but `x is y` returns `False` because they are two distinct objects in memory. 103 | 104 | --- 105 | 106 | This explanation covers the basics of membership and identity operators in Python, including their syntax, purpose, and practical examples. -------------------------------------------------------------------------------- /06-control_flow/06e_loops/01_for_loop/README.md: -------------------------------------------------------------------------------- 1 | # For Loop 2 | 3 | The for loop is used to iterate over a sequence (like a list, tuple, set, or string) or any other iterable object. It allows you to execute a block of code a specific number of times, usually determined by the length of the sequence or the range of values. 4 | 5 | ### Syntax 6 | 7 | ``` 8 | for item in iterable: 9 | # Execute this block of code 10 | ``` 11 | 12 | ### Iterating over a list 13 | 14 | ``` 15 | fruits = ["apple", "banana", "cherry"] 16 | for fruit in fruits: 17 | print(fruit) 18 | ``` 19 | 20 | ### Iterating over a string 21 | 22 | ``` 23 | for letter in "Python": 24 | print(letter) 25 | ``` 26 | 27 | ### Using range() in a for loop 28 | 29 | ``` 30 | for i in range(5): 31 | print(i) 32 | ``` 33 | 34 | ### Specifying a start and end in range() 35 | 36 | ``` 37 | for i in range(2, 6): 38 | print(i) 39 | ``` 40 | 41 | ### Using a step in range() 42 | 43 | ``` 44 | for i in range(1, 10, 2): 45 | print(i) 46 | ``` 47 | 48 | ### for Loop with zip() 49 | Iterating over two lists simultaneously 50 | 51 | ``` 52 | names = ["Alice", "Bob", "Charlie"] 53 | ages = [25, 30, 35] 54 | 55 | for name, age in zip(names, ages): 56 | print(f"{name} is {age} years old") 57 | ``` 58 | 59 | --- 60 | 61 | # Nested for Loops 62 | 63 | Nested for loops are used when you need to perform an action that involves iterating over multiple sequences or when dealing with multi-dimensional data (like a matrix or list of lists). 64 | 65 | 66 | ### Iterating over a list of lists 67 | 68 | ``` 69 | matrix = [ 70 | [1, 2, 3], 71 | [4, 5, 6], 72 | [7, 8, 9] 73 | ] 74 | 75 | for row in matrix: 76 | for element in row: 77 | print(element, end=" ") 78 | print() 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /06-control_flow/06e_loops/01_for_loop/code.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "friends: list[str] = [\"rehan\", \"musa\", \"usman\", \"ali\"]\n", 10 | "\n", 11 | "for friend in friends:\n", 12 | " print(f\"Hello! {friend}, how are you\")\n", 13 | "\n", 14 | "print(friend)" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "for i in range(5): # [ 0, 1, 2, 3, 4]\n", 24 | " print(f\"Hello world, iteration No:{i}\")" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "for i in range(2, 11): # [ 2, 3,...,10]\n", 34 | " print(f\"Hello world, iteration No:{i}\")" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "for i in range(2, 11, 2): # [ 2, 3,...,10]\n", 44 | " print(f\"Hello world, iteration No:{i}\")" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "names = [\"Alice\", \"Bob\", \"Charlie\"]\n", 54 | "ages = [25, 30, 35, 45]\n", 55 | "\n", 56 | "for name, age in zip(names, ages, strict=True):\n", 57 | " print(f\"{name} is {age} years old\")" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "values = [\n", 67 | " [1, 2, 3],\n", 68 | " [4, 5, 6],\n", 69 | " [7, 8, 9]\n", 70 | "]\n", 71 | "\n", 72 | "for value in values:\n", 73 | " for sub_value in value:\n", 74 | " print(f\"InnerLoop: {sub_value}\")\n", 75 | "\n", 76 | " print(f\"Outerloop: {value}\")\n", 77 | "\n", 78 | "print(\"Loop finished\")" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "# 2 to 10\n", 88 | "\n", 89 | "for table_nmbr in range(2, 100):\n", 90 | " for inner_val in range(1, 11):\n", 91 | " print(f\" {table_nmbr} X {inner_val} = {table_nmbr * inner_val}\")\n", 92 | " print(\"\")\n", 93 | "\n", 94 | "\n", 95 | "# 2 x 1 = 2\n", 96 | "# table_nmbr X inner_val = output" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [] 105 | } 106 | ], 107 | "metadata": { 108 | "language_info": { 109 | "name": "python" 110 | } 111 | }, 112 | "nbformat": 4, 113 | "nbformat_minor": 2 114 | } 115 | -------------------------------------------------------------------------------- /06-control_flow/06e_loops/02_while_loop/README.md: -------------------------------------------------------------------------------- 1 | # The While Loop: 2 | 3 | ```json 4 | while you have peanuts 5 | keep eating 6 | ``` 7 | - `while` and `if` both checks the conditions. If the condition is met (`True`), the instructions in `if` block exeuctures only once. However, in `while`, if the condition is met (`True`), the instructions in the `while` block runs more than once. 8 | 9 | - If condition evaluates to `False` the instuctions in the block never runs. 10 | 11 | - There should be a mechanism in the body to change the condition value. Otherwise the body will keep executing forever. 12 | 13 | ## Situation 14 | You have a class that runs until 6 PM. During this time, you want to keep learn Python. You don’t want to stop coding until the clock hits 6 PM. 15 | 16 | 17 | In a real-life scenario, you might keep checking the time every few minutes while you’re coding to see if it’s 6 PM yet: 18 | 19 | 1. Start coding. 20 | 2. Check the time. 21 | 3. If it’s not 6 PM, continue coding. 22 | 4. Repeat this process until it’s 6 PM. 23 | 24 | This approach works, but it requires constant attention to the clock, which can be distracting. 25 | 26 | # While Loop 27 | The while loop in Python is a control flow statement that allows code to be executed repeatedly based on a given condition. The loop continues to execute as long as the condition evaluates to True. Once the condition becomes False, the loop stops running. 28 | 29 | #### syntax 30 | ``` 31 | while condition: 32 | # Code block to be executed repeatedly 33 | ``` 34 | 35 | In a programming context, you can automate this process using a while loop that will allow you to keep coding until the time reaches 6 PM. 36 | 37 | ``` 38 | while current_time < 18: # 18 represents 6 PM in 24-hour format 39 | print("Still coding... The time is not yet 6 PM.") 40 | 41 | print("It's 6 PM! Time to stop coding and end the class.") 42 | ``` 43 | 44 | --- 45 | 46 | ### Example 1: Basic while Loop 47 | 48 | ``` 49 | count = 0 50 | while count < 5: 51 | print("Count is:", count) 52 | count += 1 53 | ``` 54 | Explanation: 55 | 56 | • The loop starts with count equal to 0. 57 | • It prints the value of count and then increments count by 1. 58 | • This process repeats until count is no longer less than 5. 59 | 60 | 61 | ### Example 2: while Loop with a Condition 62 | You can use a while loop to keep prompting the user until they enter a valid input. 63 | 64 | ``` 65 | password = "" 66 | while password != "Pass123": 67 | password = input("Enter the password: ") 68 | 69 | print("Access granted") 70 | ``` 71 | 72 | 73 | ### Example 3: Infinite while Loop 74 | A while loop can run indefinitely if the condition never becomes False. This is known as an infinite loop, and it will continue to run until you manually stop it or break out of it with a break statement. 75 | 76 | ``` 77 | while True: 78 | print("This loop will run forever") 79 | # You can include a break condition to exit the loop 80 | ``` 81 | 82 | ***Note: Be careful with infinite loops, as they can cause your program to hang if not properly managed.*** 83 | 84 | 85 | -------------------------------------------------------------------------------- /06-control_flow/06e_loops/02_while_loop/code.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "condition = \"\"\n", 10 | "while condition != 'exit':\n", 11 | " user_in = input(\"Enter command: \")\n", 12 | " if user_in != 'exit':\n", 13 | " print(user_in)\n", 14 | " elif user_in == 'exit':\n", 15 | " condition = user_in" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "metadata": {}, 22 | "outputs": [], 23 | "source": [ 24 | "count = 0\n", 25 | "while count < 5:\n", 26 | " print(\"Count is:\", count)\n", 27 | " count = count + 1" 28 | ] 29 | } 30 | ], 31 | "metadata": { 32 | "language_info": { 33 | "name": "python" 34 | } 35 | }, 36 | "nbformat": 4, 37 | "nbformat_minor": 2 38 | } 39 | -------------------------------------------------------------------------------- /06-control_flow/06e_loops/02_while_loop/main.py: -------------------------------------------------------------------------------- 1 | condition = "" 2 | while condition != 'exit': 3 | user_in = input("Enter command: ") 4 | if user_in != 'exit': 5 | print(user_in) 6 | elif user_in == 'exit': 7 | condition = user_in -------------------------------------------------------------------------------- /06-control_flow/06e_loops/03_loop_control_statements/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Python provides some statements to control the flow of loops: 3 | 4 | The continue and break statements are control flow tools in Python that allow you to alter the behavior of loops (for and while loops). They provide ways to skip iterations or exit loops prematurely based on specific conditions. 5 | 6 | ### 1. break Statement 7 | 8 | The break statement is used to exit a loop immediately, regardless of the loop’s condition. When break is encountered, the loop terminates, and the program continues with the next statement after the loop. 9 | 10 | ***Use in a for Loop*** 11 | 12 | ``` 13 | for i in range(1, 11): 14 | if i == 5: 15 | break # Exit the loop when i is 5 16 | print(i) 17 | ``` 18 | 19 | ***Use in a while Loop*** 20 | 21 | ``` 22 | count = 0 23 | while count < 10: 24 | print(count) 25 | count += 1 26 | if count == 5: 27 | break # Exit the loop when count is 5 28 | ``` 29 | 30 | 31 | ### 2. continue Statement 32 | 33 | The continue statement is used to skip the current iteration of a loop and proceed with the next iteration. When continue is encountered, the loop doesn’t terminate; it just skips the remaining code in the current iteration and moves on to the next iteration. 34 | 35 | ***Use in a for Loop*** 36 | ``` 37 | for i in range(1, 6): 38 | if i == 3: 39 | continue # Skip the iteration when i is 3 40 | print(i) 41 | 42 | ``` 43 | 44 | ***Use in a while Loop*** 45 | 46 | ``` 47 | count = 0 48 | while count < 5: 49 | count += 1 50 | if count == 3: 51 | continue # Skip the rest of the loop when count is 3 52 | print(count) 53 | ``` 54 | 55 | 56 | When to Use break and continue: 57 | 58 | • break: 59 | • Exiting a loop when a certain condition is met (e.g., finding an item in a list and stopping further search). 60 | • Preventing infinite loops when a certain condition occurs. 61 | • continue: 62 | • Skipping certain values in a loop (e.g., skipping over unwanted data or values that don’t need processing). 63 | • Avoiding unnecessary computations or actions within specific loop iterations. 64 | 65 | 66 | Both break and continue are powerful tools for managing the flow of loops, allowing you to fine-tune how and when specific blocks of code are executed. 67 | -------------------------------------------------------------------------------- /06-control_flow/06e_loops/03_loop_control_statements/code.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "for nmber in range(1, 18):\n", 10 | " if (nmber == 10):\n", 11 | " continue\n", 12 | " print(f'iteration number : {nmber}')" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "for table_nmbr in range(2, 10):\n", 22 | " for inner_val in range(1, 11):\n", 23 | " if (table_nmbr == 3 and inner_val > 5):\n", 24 | " continue\n", 25 | " print(f\" {table_nmbr} X {inner_val} = {table_nmbr * inner_val}\")\n", 26 | " print(\"\")" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "for nmber in range(1, 18):\n", 36 | " if (nmber == 10):\n", 37 | " break\n", 38 | " print(f'iteration number : {nmber}')" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "numbers: list[int] = [2, 4, 6, 8]\n", 48 | "squares: list[int] = []\n", 49 | "\n", 50 | "for number in numbers:\n", 51 | " square = number**2\n", 52 | " squares.append(square)\n", 53 | " # squares.append(number**2)\n", 54 | "\n", 55 | "print(squares)" 56 | ] 57 | } 58 | ], 59 | "metadata": { 60 | "language_info": { 61 | "name": "python" 62 | } 63 | }, 64 | "nbformat": 4, 65 | "nbformat_minor": 2 66 | } 67 | -------------------------------------------------------------------------------- /06-control_flow/06e_loops/README.md: -------------------------------------------------------------------------------- 1 | ## The Problem: 2 | 3 | Imagine you’re organizing a birthday party and you want to send an invitation message to 50 of your friends. Without any tools or shortcuts, you would have to write the same message 50 times, changing only the name of each friend. This is repetitive and time-consuming. 4 | 5 | you would have to manually send each message like this: 6 | 7 | ``` 8 | print("Hi Alice, you’re invited to my birthday party!") 9 | print("Hi Bob, you’re invited to my birthday party!") 10 | print("Hi Charlie, you’re invited to my birthday party!") 11 | # ...and so on, for 50 friends 12 | ``` 13 | 14 | If you have a long list of friends, this approach quickly becomes impractical. Not only does it take a lot of time, but if you want to change the message later, you’d have to edit each line individually. 15 | 16 | ## The Solution: Using Loops to Automate Invitations 17 | 18 | Now, let’s say you want to automate this process. You can use a loop to send the same message to all your friends with just a few lines of code 19 | 20 | ``` 21 | friends = ["Alice", "Bob", "Charlie", "David", "Eve"] # and so on, up to 50 names 22 | 23 | for friend in friends: 24 | print(f"Hi {friend}, you’re invited to my birthday party!") 25 | 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /07-tuples_dictionaries/07a-list_comprehension/code.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "numbers: list[int] = [2, 4, 6, 8]\n", 10 | "squares: list[int] = []\n", 11 | "\n", 12 | "for number in numbers:\n", 13 | " square = number**2\n", 14 | " squares.append(square)\n", 15 | " # squares.append(number**2)\n", 16 | "\n", 17 | "print(squares)" 18 | ] 19 | }, 20 | { 21 | "cell_type": "code", 22 | "execution_count": null, 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "# [expression for item in iterable]\n", 27 | "squares2 = [number**2 for number in numbers]\n", 28 | "print(squares2)" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": null, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "# we will find squares of even number from 1 to 10\n", 38 | "\n", 39 | "# 1. use range function\n", 40 | "# 2. for loop\n", 41 | "# 3. condtional statement to find even numbers\n", 42 | "# 4. calcualte the squares of even numebrs\n", 43 | "\n", 44 | "squares3: list[int] = []\n", 45 | "for i in range(1, 11):\n", 46 | " if i % 2 == 0:\n", 47 | " square = i**2\n", 48 | " squares3.append(square)\n", 49 | "\n", 50 | "print(squares3)" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "square4 = [i**2 for i in range(1, 11) if i % 2 == 0]\n", 60 | "print(square4)" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "print(dir(str))\n", 70 | "methods = [method for method in dir(str) if '__' not in method]\n", 71 | "print(methods)" 72 | ] 73 | } 74 | ], 75 | "metadata": { 76 | "language_info": { 77 | "name": "python" 78 | } 79 | }, 80 | "nbformat": 4, 81 | "nbformat_minor": 2 82 | } 83 | -------------------------------------------------------------------------------- /07-tuples_dictionaries/07a-list_comprehension/readme.md: -------------------------------------------------------------------------------- 1 | # Introduction to List Comprehensions in Python 2 | 3 | ## What is List Comprehension? 4 | 5 | List comprehension is a concise way to create lists in Python. It allows us to generate a new list by applying an expression to each item in an existing iterable (such as a list, tuple, or range) and optionally applying a condition to filter the items. 6 | 7 | ## Why Do We Need List Comprehensions? 8 | 9 | ### The Problem: Creating Lists Using Loops 10 | 11 | Consider a scenario where we want to create a list of squares for the numbers 1 through 5. A typical approach might involve using a `for` loop: 12 | 13 | ```python 14 | squares : list[int] = [] 15 | for x in range(1, 6): 16 | squares.append(x**2) 17 | 18 | print(squares) # Output: [1, 4, 9, 16, 25] 19 | ``` 20 | 21 | While this works, it requires multiple lines of code: one to initialize the list, one for the loop, and one to append each result. As the complexity of the list generation increases, so does the amount of code needed. 22 | 23 | ### The Solution: Simplify Code with List Comprehensions 24 | 25 | List comprehensions provide a more elegant and concise way to achieve the same result. We can generate the list of squares in a single line: 26 | 27 | ```python 28 | squares : list[int] = [x**2 for x in range(1, 6)] 29 | print(squares) # Output: [1, 4, 9, 16, 25] 30 | ``` 31 | 32 | This approach is not only more readable but also often more efficient. 33 | 34 | ## How List Comprehensions Work 35 | 36 | ### Basic Syntax 37 | 38 | The basic syntax of a list comprehension is: 39 | 40 | ```python 41 | [expression for item in iterable] 42 | ``` 43 | 44 | Where: 45 | - `expression` is the value to be included in the new list. 46 | - `item` represents each element in the `iterable` (e.g., list, range, etc.). 47 | - `iterable` is the collection or range we're iterating over. 48 | 49 | ### Example: Creating a List of Squares 50 | 51 | ```python 52 | squares : list[int] = [x**2 for x in range(1, 6)] 53 | print(squares) # Output: [1, 4, 9, 16, 25] 54 | ``` 55 | 56 | ### Adding Conditions: Filtering with List Comprehensions 57 | 58 | We can add an optional condition to filter the elements being processed by the list comprehension. 59 | 60 | #### Example: Creating a List of Even Squares 61 | 62 | ```python 63 | even_squares : list[int] = [x**2 for x in range(1, 11) if x % 2 == 0] 64 | print(even_squares) # Output: [4, 16, 36, 64, 100] 65 | ``` 66 | 67 | In this example, the list comprehension includes only even numbers (i.e., `x % 2 == 0`) before calculating the square. 68 | 69 | ## More Advanced Examples 70 | 71 | ### Nested List Comprehensions 72 | 73 | We can use nested list comprehensions to create lists of lists or perform more complex operations. 74 | 75 | 76 | ### Flattening a List of Lists 77 | 78 | We can flatten a list of lists into a single list using a list comprehension. 79 | 80 | #### Example: 81 | 82 | ```python 83 | matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 84 | flattened = [num for row in matrix for num in row] 85 | print(flattened) # Output: [1, 2, 3, 4, 5, 6, 7, 8, 9] 86 | ``` 87 | 88 | ## Advantages of List Comprehensions 89 | 90 | ### 1. Conciseness 91 | 92 | List comprehensions allow us to create lists in a single line of code, reducing the need for boilerplate code like loops and list initialization. 93 | 94 | ### 2. Readability 95 | 96 | When used appropriately, list comprehensions can make our code more readable by clearly showing the intent of the list creation in a compact format. 97 | 98 | ### 3. Efficiency 99 | 100 | List comprehensions are often more efficient than traditional loops, as they are optimized for performance in Python. 101 | 102 | ## Conclusion 103 | 104 | List comprehensions are a powerful feature in Python that allows for clean, concise, and efficient list creation. Whether we're generating simple lists or working with more complex data transformations, list comprehensions can significantly streamline our code. 105 | -------------------------------------------------------------------------------- /07-tuples_dictionaries/07b-tuple/code.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "my_tuple = (1, 2, 3)\n", 10 | "print(my_tuple)\n", 11 | "print(type(my_tuple))" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "my_tuple1 = 1, 2, 3\n", 21 | "print(my_tuple1)\n", 22 | "print(type(my_tuple1))" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": null, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "my_tuple2 = (1)\n", 32 | "print(my_tuple2)\n", 33 | "print(type(my_tuple2))" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "my_tuple3 = (1,)\n", 43 | "print(my_tuple3)\n", 44 | "print(type(my_tuple3))" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "tup: tuple[int, ...] = (1, 2, 3, 4, 5, 6)\n", 54 | "print(tup[0])" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "tup1: tuple[int, ...] = (1, 2, 3, 4, 5, 6)\n", 64 | "# tup1[0] = 10\n", 65 | "print(len(tup1))" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": null, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "names: tuple[str, str] = (\"Rehan\", \"Usman\")\n", 75 | "\n", 76 | "name1, name2 = names\n", 77 | "print(name1)\n", 78 | "print(name2)\n", 79 | "print(type(name1))\n", 80 | "print(type(name2))\n", 81 | "\n", 82 | "print(names)" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "a = 5\n", 92 | "b = 10\n", 93 | "c = a\n", 94 | "a = b\n", 95 | "b = c\n", 96 | "print(a)\n", 97 | "print(b)" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "a = 5\n", 107 | "b = 10\n", 108 | "a, b = b, a\n", 109 | "print(a)\n", 110 | "print(b)" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "t1 = (1, 2)\n", 120 | "t2 = (2, 3)\n", 121 | "print(f\"Before swapping: t1={t1} and t2={t2}\")\n", 122 | "t1, t2 = t2, t1\n", 123 | "print(f\"After swapping: t1={t1} and t2={t2}\")" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": null, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "# Assignment: Try to change the elements in the list.\n", 133 | "test_tup = (1, 2, [100, 200])" 134 | ] 135 | } 136 | ], 137 | "metadata": { 138 | "language_info": { 139 | "name": "python" 140 | } 141 | }, 142 | "nbformat": 4, 143 | "nbformat_minor": 2 144 | } 145 | -------------------------------------------------------------------------------- /07-tuples_dictionaries/07b-tuple/readme.md: -------------------------------------------------------------------------------- 1 | # Introduction to Tuples in Python 2 | A sequence is a type of data in python which is able to store more than one value. These values can be sequentially browsed i.e. element by element. 3 | 4 | So far, we've only studied one type of sequence i.e. `list`. Here we'll explore another type of sequene called `tuple`. 5 | 6 | We've already discussed the concept of **Immutability** and **Mutability**. Let's revise it. 7 | 8 | - *Immutabale* means can't be changed/modified/updated. Example: Primitive Data types 9 | - *Mutable* means can be changed/modified/updated. Example: Lists in Python 10 | 11 | 12 | ## What is a Tuple? 13 | 14 | A tuple is an immutable, ordered collection of elements in Python. Like lists, tuples can hold a sequence of elements, but unlike lists, tuples cannot be changed (i.e., modified) after they are created. Tuples are typically used to group together related data. 15 | 16 | ## Why Do We Need Tuples? 17 | ### The Problem: Need for Immutable Collections 18 | 19 | In programming, there are situations where we want to ensure that a sequence of values remains constant throughout the program. For example, we might have a set of coordinates or configuration values that should not be altered accidentally. 20 | 21 | Using a list for such cases is risky because lists are mutable, meaning their content can be changed. This is where tuples come in handy. 22 | 23 | ### The Solution: Using Tuples for Immutable Sequences 24 | 25 | Tuples provide a way to create a sequence of elements that cannot be modified. This immutability makes tuples useful in scenarios where the integrity of the data must be preserved. 26 | 27 | ```python 28 | coordinates : tuple[int,int] = (10, 20) 29 | print(coordinates) # Output: (10, 20) 30 | 31 | # Trying to modify a tuple will raise an error 32 | # coordinates[0] = 15 # This will raise a TypeError 33 | ``` 34 | 35 | ## How Tuples Work 36 | 37 | ### Basic Syntax 38 | 39 | Tuples are created by placing a sequence of elements separated by commas within parentheses: 40 | 41 | ```python 42 | my_tuple : tuple[int, int, int] = (1, 2, 3) 43 | ``` 44 | 45 | ### Example: Storing Multiple Values 46 | 47 | Tuples can be used to store related data, such as the x, y, and z coordinates of a point in space: 48 | 49 | ```python 50 | point : tuple[int, int, int] = (1, 2, 3) 51 | print(point) # Output: (1, 2, 3) 52 | ``` 53 | 54 | ### Creating a Tuple Without Parentheses 55 | 56 | We can also create a tuple without using parentheses by simply separating the elements with commas: 57 | 58 | ```python 59 | my_tuple : tuple[int, int, int] = 1, 2, 3 60 | print(my_tuple) # Output: (1, 2, 3) 61 | ``` 62 | 63 | ### Single-Element Tuples 64 | 65 | To create a tuple with a single element, we must include a trailing comma: 66 | 67 | ```python 68 | single_element_tuple = (5,) 69 | print(single_element_tuple) # Output: (5,) 70 | ``` 71 | 72 | Without the comma, Python will not recognize it as a tuple: 73 | 74 | ```python 75 | not_a_tuple = (5) 76 | print(type(not_a_tuple)) # Output: 77 | ``` 78 | 79 | ## Operations with Tuples 80 | 81 | ### Accessing Elements 82 | 83 | We can access elements in a tuple using indexing, similar to lists: 84 | 85 | ```python 86 | my_tuple = ('apple', 'banana', 'cherry') 87 | print(my_tuple[1]) # Output: banana 88 | ``` 89 | 90 | ### Slicing Tuples 91 | 92 | We can slice tuples to get a range of elements: 93 | 94 | ```python 95 | my_tuple = ('apple', 'banana', 'cherry', 'date') 96 | print(my_tuple[1:3]) # Output: ('banana', 'cherry') 97 | ``` 98 | 99 | ### Tuple Concatenation and Repetition 100 | 101 | Tuples can be concatenated using the `+` operator and repeated using the `*` operator: 102 | 103 | ```python 104 | # Concatenation 105 | tuple1 = (1, 2) 106 | tuple2 = (3, 4) 107 | result = tuple1 + tuple2 108 | print(result) # Output: (1, 2, 3, 4) 109 | 110 | # Repetition 111 | repeated_tuple = ('A',) * 3 112 | print(repeated_tuple) # Output: ('A', 'A', 'A') 113 | ``` 114 | 115 | ### Tuple Unpacking 116 | 117 | Tuple unpacking allows us to assign each element of a tuple to a variable in a single statement: 118 | 119 | ```python 120 | scores = (10, 20) 121 | x, y = scores 122 | print(x) # Output: 10 123 | print(y) # Output: 20 124 | ``` 125 | 126 | ### Nesting Tuples 127 | 128 | Tuples can contain other tuples, which is useful for organizing data hierarchically: 129 | 130 | ```python 131 | nested_tuple = (1, (2, 3), (4, 5, 6)) 132 | print(nested_tuple) # Output: (1, (2, 3), (4, 5, 6)) 133 | ``` 134 | 135 | ## Advantages of Tuples 136 | 137 | ### 1. Immutability 138 | 139 | The immutability of tuples ensures that the data remains consistent and secure, especially in cases where the data should not be altered. 140 | 141 | ### 2. Performance 142 | 143 | Tuples are generally faster than lists for iterating through elements because they are immutable and therefore require less memory. 144 | 145 | ### 3. Use as Dictionary Keys 146 | 147 | Because tuples are immutable, they can be used as keys in dictionaries, unlike lists. 148 | 149 | ```python 150 | location = {} 151 | point = (10, 20) 152 | location[point] = "Park" 153 | print(location) # Output: {(10, 20): 'Park'} 154 | ``` 155 | 156 | ## Conclusion 157 | 158 | Tuples are a fundamental data structure in Python that provide a way to store ordered, immutable collections of elements. They are ideal for situations where we need a sequence of elements that should not be changed, and they offer several advantages in terms of performance and data integrity. 159 | 160 | 161 | ## Tuple Methods and Operations 162 | 163 | ### 1. `count()` 164 | 165 | The `count()` method returns the number of occurrences of a specified value in the tuple. 166 | 167 | #### Example: 168 | 169 | ```python 170 | my_tuple = (1, 2, 3, 2, 2, 4) 171 | count_of_twos = my_tuple.count(2) 172 | print(count_of_twos) # Output: 3 173 | ``` 174 | 175 | ### 2. `index()` 176 | 177 | The `index()` method returns the index of the first occurrence of a specified value. If the value is not found, it raises a `ValueError`. 178 | 179 | #### Example: 180 | 181 | ```python 182 | my_tuple = ('apple', 'banana', 'cherry') 183 | index_of_banana = my_tuple.index('banana') 184 | print(index_of_banana) # Output: 1 185 | ``` 186 | 187 | ### Tuple Operations 188 | 189 | #### 1. Accessing Elements 190 | 191 | We can access elements of a tuple using square brackets `[]` with the index of the element. Remember that tuple indices start at 0. 192 | 193 | ```python 194 | my_tuple = ('apple', 'banana', 'cherry') 195 | print(my_tuple[0]) # Output: apple 196 | ``` 197 | 198 | #### 2. Slicing 199 | 200 | We can slice a tuple to get a subset of elements: 201 | 202 | ```python 203 | my_tuple = ('apple', 'banana', 'cherry', 'date') 204 | print(my_tuple[1:3]) # Output: ('banana', 'cherry') 205 | ``` 206 | 207 | #### 3. Concatenation 208 | 209 | Tuples can be concatenated using the `+` operator: 210 | 211 | ```python 212 | tuple1 = (1, 2, 3) 213 | tuple2 = (4, 5, 6) 214 | result = tuple1 + tuple2 215 | print(result) # Output: (1, 2, 3, 4, 5, 6) 216 | ``` 217 | 218 | #### 4. Repetition 219 | 220 | We can repeat a tuple a certain number of times using the `*` operator: 221 | 222 | ```python 223 | my_tuple = ('A', 'B') 224 | repeated_tuple = my_tuple * 3 225 | print(repeated_tuple) # Output: ('A', 'B', 'A', 'B', 'A', 'B') 226 | ``` 227 | 228 | #### 5. Membership Test 229 | 230 | We can check if an item exists in a tuple using the `in` keyword: 231 | 232 | ```python 233 | my_tuple = ('apple', 'banana', 'cherry') 234 | print('banana' in my_tuple) # Output: True 235 | print('grape' in my_tuple) # Output: False 236 | ``` 237 | 238 | #### 6. Iterating Over a Tuple 239 | 240 | We can iterate over the elements of a tuple using a `for` loop: 241 | 242 | ```python 243 | my_tuple = ('apple', 'banana', 'cherry') 244 | for fruit in my_tuple: 245 | print(fruit) 246 | # Output: 247 | # apple 248 | # banana 249 | # cherry 250 | ``` 251 | 252 | ### Tuple Usage Examples 253 | 254 | 255 | #### 1. Swapping Variables 256 | 257 | Tuples can be used for swapping the values of two variables without needing a temporary variable. 258 | 259 | ```python 260 | a = 5 261 | b = 10 262 | a, b = b, a 263 | print(a) # Output: 10 264 | print(b) # Output: 5 265 | ``` 266 | #### 2. Swapping Tuples 267 | Tuples themselves can be swapped. 268 | 269 | ```python 270 | t1 = (1,2) 271 | t2 = (2,3) 272 | print(f"Before swapping: t1={t1} and t2={t2}") # Output: Before swapping: t1=(1, 2) and t2=(2, 3) 273 | t1, t2 = t2, t1 274 | print(f"After swapping: t1={t1} and t2={t2}") # Output: After swapping: t1=(2, 3) and t2=(1, 2) 275 | ``` 276 | 277 | #### 3. Storing Related Data 278 | 279 | Tuples can store related data, like coordinates or RGB color values. 280 | 281 | ```python 282 | point = (10, 20) 283 | color = (255, 0, 0) 284 | 285 | print(point) # Output: (10, 20) 286 | print(color) # Output: (255, 0, 0) 287 | ``` 288 | 289 | #### 4. Using Tuples as Dictionary Keys 290 | 291 | Because tuples are immutable, they can be used as keys in dictionaries. 292 | 293 | ```python 294 | locations = {} 295 | point = (10, 20) 296 | locations[point] = "Park" 297 | print(locations) # Output: {(10, 20): 'Park'} 298 | ``` 299 | 300 | 301 | -------------------------------------------------------------------------------- /07-tuples_dictionaries/07c-dictionary/readme.md: -------------------------------------------------------------------------------- 1 | # Introduction to Dictionaries in Python 2 | 3 | ## What is a Dictionary? 4 | 5 | Dictionary, a data structure in Python, is a collection of *key-value pairs*, where each *key is unique*, and each key is associated with a specific value. Unlike other data structures in Python, like lists or sets, *dictionaries are unordered*, meaning that the items are not stored in any particular sequence. 6 | 7 | Dictionary enable the association of values with unique keys, providing a way to store and retrieve information using meaningful identifiers. Dictionaries are particularly valuable when there is a need for fast data access and retrieval based on specific keys. They are versatile and widely used in scenarios where data needs to be stored and accessed in a structured manner. 8 | 9 | ## Why Do We Need Dictionaries? 10 | 11 | ### The Problem: Efficient Data Lookups 12 | 13 | Consider a scenario where 're managing a collection of student scores in different subjects. Initially, might think of using two separate lists—one for student names and one for their corresponding scores: 14 | 15 | ```python 16 | students : list[str ]= ["Alice", "Bob", "Charlie"] 17 | scores : list[int] = [85, 92, 78] 18 | ``` 19 | 20 | To find the score of a specific student, would need to first find the index of the student's name in the `students` list and then use that index to look up the score in the `scores` list: 21 | 22 | ```python 23 | index : int = students.index("Alice") 24 | score : int = scores[index] 25 | ``` 26 | 27 | While this approach works, it quickly becomes inefficient as the lists grow in size. Searching for a student's name in the list takes time, and managing two separate lists can lead to errors, especially if they become out of sync. 28 | 29 | ### The Solution: Constant-Time Lookups with Dictionaries 30 | 31 | Dictionaries provide a more efficient and intuitive way to handle this situation. By using student names as keys and their scores as values, can store the data in a dictionary: 32 | 33 | ```python 34 | students_scores : dict{str,int} = { 35 | "Alice": 85, 36 | "Bob": 92, 37 | "Charlie": 78 38 | } 39 | ``` 40 | 41 | Now, finding a student's score is much simpler and faster: 42 | 43 | ```python 44 | score : int = students_scores["Alice"] 45 | ``` 46 | 47 | This operation is performed in constant time, regardless of the number of students. 48 | 49 | ## What Problems Do Dictionaries Solve? 50 | 51 | ### 1. Fast Data Retrieval 52 | 53 | Dictionaries are optimized for fast data retrieval. Instead of searching through an entire list, can quickly access any value directly by its key. 54 | 55 | ### 2. Clearer, More Expressive Code 56 | 57 | Dictionaries allow us to write clearer and more expressive code. The key-value structure makes it obvious what each item represents, improving code readability. 58 | 59 | ### 3. Flexibility in Data Organization 60 | 61 | Dictionaries offer flexibility in how organize data. can store complex structures, like lists or other dictionaries, as values, enabling us to represent nested or hierarchical data. 62 | 63 | ## Working with Dictionaries in Python 64 | 65 | ### Important Properties 66 | - Each key should be unique 67 | - Key can be any immutable type of object 68 | - `len()` function also works with dictionaries i.e. returns the length of key-value pairs 69 | - A dictionary is a one way tool i.e. We can find the value from key but cannot find they key from value. It works the same way as original dictionary.We can find urdu menaing of word in english but not the english meaning of urdu word. 70 | - The dictionaries are not ordered. We might have different orders when we use `print()` function. 71 | - Dictionaries are not a sequence type. So can we use for loop with dictionaries? No! and Yes! We'll see it in below examples. 72 | 73 | ### Creating a Dictionary 74 | 75 | can create a dictionary using curly braces `{}` or the `dict()` function. 76 | 77 | #### Example: 78 | ```python 79 | # Using curly braces 80 | student_scores : dict[str, int] = { 81 | "Alice": 85, 82 | "Bob": 92, 83 | "Charlie": 78 84 | } 85 | 86 | # Using dict() function 87 | student_scores : dict[str,int] = dict(Alice=85, Bob=92, Charlie=78) 88 | ``` 89 | 90 | ### Accessing Values 91 | 92 | can access values in a dictionary using the key inside square brackets `[]` or with the `.get()` method. 93 | 94 | #### Example: 95 | ```python 96 | # Accessing a value using a key 97 | print(student_scores["Alice"]) # Output: 85 98 | 99 | # Using the get() method 100 | print(student_scores.get("Bob")) # Output: 92 101 | ``` 102 | 103 | #### Handling Missing Keys 104 | The `.get()` method is safer for accessing keys, as it returns `None` or a default value if the key is not found, rather than raising a `KeyError`. 105 | 106 | #### Example: 107 | ```python 108 | # Using square brackets (raises KeyError if key is not found) 109 | # print(student_scores["David"]) # Uncommenting this line would raise KeyError 110 | 111 | # Using get() (returns None if key is not found) 112 | print(student_scores.get("David")) # Output: None 113 | 114 | # Providing a default value with get() 115 | print(student_scores.get("David", "Not Found")) # Output: Not Found 116 | ``` 117 | 118 | ## Adding and Updating Items 119 | 120 | can add a new key-value pair or update an existing one using square brackets `[]`. 121 | 122 | #### Example: 123 | ```python 124 | # Adding a new key-value pair 125 | student_scores["David"] = 88 126 | print(student_scores) # Output: {'Alice': 85, 'Bob': 92, 'Charlie': 78, 'David': 88} 127 | 128 | # Updating an existing key-value pair 129 | student_scores["Alice"] = 90 130 | print(student_scores) # Output: {'Alice': 90, 'Bob': 92, 'Charlie': 78, 'David': 88} 131 | 132 | ``` 133 | 134 | ### Removing Items 135 | 136 | can remove items using the `del` statement, the `.pop()` method, or the `.popitem()` method. 137 | 138 | #### Example: 139 | ```python 140 | # Removing a specific key-value pair using del 141 | del student_scores["Charlie"] 142 | print(student_scores) # Output: {'Alice': 90, 'Bob': 92, 'David': 88} 143 | 144 | # Removing a specific key-value pair using pop() 145 | removed_score = student_scores.pop("David") 146 | print(removed_score) # Output: 88 147 | print(student_scores) # Output: {'Alice': 90, 'Bob': 92} 148 | 149 | # Removing the last inserted key-value pair using popitem() 150 | last_item = student_scores.popitem() 151 | print(last_item) # Output: ('Bob', 92) 152 | print(student_scores) # Output: {'Alice': 90} 153 | ``` 154 | 155 | ## Checking if a Key Exists 156 | 157 | can check if a key exists in a dictionary using the `in` keyword. 158 | 159 | #### Example: 160 | ```python 161 | print("Alice" in student_scores) # Output: True 162 | print("Charlie" in student_scores) # Output: False 163 | ``` 164 | 165 | ### Iterating Through a Dictionary 166 | 167 | We can iterate through the keys, values, or key-value pairs in a dictionary using a `for` loop. 168 | 169 | #### Example: 170 | ```python 171 | # Iterating through keys 172 | for student in student_scores: 173 | print(student) 174 | # Output: 175 | # Alice 176 | 177 | # Iterating through values 178 | for score in student_scores.values(): 179 | print(score) 180 | # Output: 181 | # 90 182 | 183 | # Iterating through key-value pairs 184 | for student, score in student_scores.items(): 185 | print(f"{student}: {score}") 186 | # Output: 187 | # Alice: 90 188 | ``` 189 | 190 | ### Dictionary Methods 191 | 192 | #### 1. `.keys()` Method 193 | Returns a view object that displays a list of all the keys in the dictionary. 194 | 195 | *Example:* 196 | ```python 197 | keys = student_scores.keys() 198 | print(keys) # Output: dict_keys(['Alice']) 199 | ``` 200 | 201 | #### 2. `.values()` Method 202 | Returns a view object that displays a list of all the values in the dictionary. 203 | 204 | *Example:* 205 | ```python 206 | values = student_scores.values() 207 | print(values) # Output: dict_values([90]) 208 | ``` 209 | 210 | #### 3. `.items()` Method 211 | Returns a view object that displays a list of the dictionary’s key-value tuple pairs. 212 | 213 | *Example:* 214 | ```python 215 | items = student_scores.items() 216 | print(items) # Output: dict_items([('Alice', 90)]) 217 | ``` 218 | 219 | #### 4. `.update()` Method 220 | Updates the dictionary with elements from another dictionary or from an iterable of key-value pairs. 221 | 222 | *Example:* 223 | ```python 224 | additional_scores = {"Eve": 95, "Frank": 87} 225 | student_scores.update(additional_scores) 226 | print(student_scores) # Output: {'Alice': 90, 'Eve': 95, 'Frank': 87} 227 | ``` 228 | 229 | #### 5. `.clear()` Method 230 | Removes all items from the dictionary. 231 | 232 | *Example:* 233 | ```python 234 | student_scores.clear() 235 | print(student_scores) # Output: {} 236 | ``` 237 | 238 | #### 5. `.copy()` Method 239 | Removes all items from the dictionary. 240 | 241 | *Example:* 242 | ```python 243 | sutdent_scroes_copy = student_scores.copy() 244 | print(sutdent_scroes_copy) # Output: {} 245 | ``` 246 | 247 | ### Dictionary Comprehension 248 | We can also apply comprehension method on dictionaries. 249 | 250 | *Example:* 251 | ```python 252 | values : dict[int,int] = {x:x for x in range(5)} 253 | print(values) # Output: {0: 0, 1: 1, 2: 2, 3: 3, 4: 4} 254 | ``` 255 | 256 | *Example:* 257 | ```python 258 | fruits : list[str] = ["apple", "banana", "orange"] 259 | fruits_dict : dict[int,str] = {i:fruit for i, fruit in enumerate(fruits)} 260 | print(fruits_dict) # Output: {0: 'apple', 1: 'banana', 2: 'orange'} 261 | ``` -------------------------------------------------------------------------------- /08-functions/08a-function_parameters/readme.md: -------------------------------------------------------------------------------- 1 | # Function Parameters 2 | In this section, we'll learn about parameterless and parameterized functions. 3 | 4 | ## What are function parameters? 5 | So far we learnt that functions perform a specific task. To perform the task, in some cases, function needs some inputs/data and in other cases, it doesn't. 6 | - Parameters simply can be said the inputs/data required by the function to perform a specific task. 7 | - The inputs/data is provided from outside the function but we have to mention them while defining the functions. 8 | - Parameters only exist inside the function. 9 | - parameters are provided in paranthesis of the function. 10 | 11 | Let's have a look at parameterless and parameterized function. 12 | 13 | ## Parameterless Fucntions: 14 | These type of functions don't require parameters. 15 | 16 | ```python 17 | def greetings(): 18 | print("Hello Rehan!") 19 | 20 | greetings() #Output: Hello Rehan! 21 | ``` 22 | 23 | ## Parameterized Fucntions: 24 | These type of functions require parameters. 25 | 26 | ```python 27 | my_name : str = "Rehan" 28 | def greetings(name): 29 | print(f"Hello {name}!") 30 | 31 | greetings(my_name) #Output: Hello Rehan! 32 | ``` 33 | - `name` here, is the parameter provided to the function. 34 | - Now our function has become more dynamic. We can provide any name, it will greet that person. 35 | - `my_name` is the argument provided at the time of function invokation. 36 | 37 | ## What are Arguments? 38 | Arguments are the actual values you pass to a function when you call it. These values get assigned to the function’s parameters. 39 | 40 | ```python 41 | greetings("Rehan") 42 | ``` 43 | `Rehan` is the argument provided to the function. 44 | - Arguments live outside the function. 45 | - Arguments can be accessed inside the function. 46 | **Parameters** 47 | - Parameters are defined in function at the time of function definition. 48 | - Parameters only live inside function. They don't have outside scope. 49 | 50 | We'll discuss it in details in `Scope` section. 51 | -------------------------------------------------------------------------------- /08-functions/08b-function_return/readme.md: -------------------------------------------------------------------------------- 1 | # Function Return 2 | ## 1. Reutrn with expression 3 | So far we only discussed the functions which perform specific task but do not provide results. Now we'll discuss the functions which do provide a result and we get this result through `return` keyword. 4 | 5 | Let's look at example of a function which takes 2 parameters (numbers), add them (perform a task) and return the result. 6 | 7 | ```python 8 | # Define a function 9 | def addition(first_number:int, second_number:int)->int: 10 | result : int = first_number + second_number 11 | return result 12 | 13 | 14 | print(addition(5, 8)) # Output: 13 15 | # Can also do like this 16 | get_sum : int = addition(5,8) 17 | print(get_sum) #Output: 13 18 | 19 | ``` 20 | If we take example of python builin functions, `print()` function doesn't return anything while `int()` returns and integer value. 21 | 22 | Here we are using return with expression. `return result` here 'result' is the expression. 23 | 24 | ## 2. Reutrn without expression 25 | Sometimes, we use `return` keyword without expression. What will function return in that case. Well, function will then return `None` 26 | 27 | **Usecase** 28 | In above example, we were just getting the sum of 2 numbers. Let's add a scenarion. We want our function to add the value only when both of the numbers are non zero. Let's implement it inside the function. 29 | 30 | ```python 31 | # Define a function 32 | def addition_non_zero(first_number:int, second_number:int)->int | None: 33 | if first_number > 0 and second_number > 0: 34 | result : int = first_number + second_number 35 | return result 36 | 37 | 38 | get_sum : int | None = addition_non_zero(5,8) 39 | print(get_sum) # Output: 13 40 | 41 | get_sum : int | None = addition_non_zero(0,8) 42 | print(get_sum) # Output: None 43 | 44 | ``` 45 | 46 | ## 3. Return Multiple values 47 | Remember, function will always return a single value. So how can we return if we need multiple values. We use tuple in that case. Function still returns a single value (a tuple), but we already know that we can unpack the tuple. 48 | 49 | **Example** 50 | ```python 51 | def mul_and_add(a:int, b:int)-> tuple[int, int]: 52 | multiplication : int = a * b 53 | addition : int = a + b 54 | return multiplication, addition 55 | 56 | result = mul_and_add(5, 4) 57 | print(result) # Output: (20, 9) 58 | 59 | # We can unpack it like so 60 | mul_result, add_result = mul_and_add(5, 4) 61 | print(f"Multiplication result:{mul_result}") 62 | print(f"Addition result:{add_result}") 63 | ``` 64 | 65 | ## 4. Return a list 66 | 67 | ```python 68 | def generate_even_numbers(limit): 69 | even_numbers = [i for i in range(limit) if i % 2 == 0] 70 | return even_numbers 71 | 72 | result = generate_even_numbers(10) 73 | print(result) # Output: [0, 2, 4, 6, 8] 74 | ``` 75 | 76 | ## 5. Return a dictionary 77 | ```python 78 | def get_student_info(name, age, course): 79 | return { 80 | "name": name, 81 | "age": age, 82 | "course": course 83 | } 84 | 85 | student_info = get_student_info("John", 21, "Mathematics") 86 | print(student_info) # Output: {'name': 'John', 'age': 21, 'course': 'Mathematics'} 87 | ``` -------------------------------------------------------------------------------- /08-functions/08c-postional_arguments/readme.md: -------------------------------------------------------------------------------- 1 | # Positional Arguments in Python Functions 2 | When defining a function, we can specify parameters that act as placeholders for values the function will receive. These values, called **arguments**, are passed when we call the function. 3 | 4 | One common way to pass arguments is using **positional arguments**. This means the arguments are assigned to parameters based on their position in the function call. 5 | 6 | 7 | ## What Are Positional Arguments? 8 | 9 | Positional arguments are arguments that are passed to a function based on the order they appear in the function call. The first argument is assigned to the first parameter, the second to the second parameter, and so on. 10 | 11 | ### Syntax: 12 | ```python 13 | def function_name(parameter1, parameter2, ...): 14 | print(f"Parameter 1: {parameter1}") 15 | print(f"Parameter 2: {parameter2}") 16 | return parameter1-parameter2 17 | ``` 18 | 19 | When calling the function, we pass values for each parameter in the same order as they were defined. 20 | 21 | ```python 22 | function_name(argument1, argument2, ...) 23 | ``` 24 | 25 | --- 26 | 27 | ## How Positional Arguments Work 28 | 29 | The following example illustrates a function that uses positional arguments: 30 | 31 | ```python 32 | def greet(name, message): 33 | print(f"Hello {name}, {message}") 34 | 35 | greet("Ali", "Welcome to Python!") # Output: Hello Ali, Welcome to Python! 36 | ``` 37 | 38 | In the example: 39 | - `"Ali"` is passed as the first argument and is assigned to the `name` parameter. 40 | - `"Welcome to Python!"` is passed as the second argument and is assigned to the `message` parameter. 41 | 42 | The position of the arguments is crucial here. If we change the order, the meaning of the arguments changes. 43 | 44 | ```python 45 | greet("Welcome to Python!", "Ali") # Output: Hello Welcome to Python!, Alice 46 | ``` 47 | 48 | --- 49 | 50 | ## Examples of Positional Arguments 51 | 52 | Here are some more examples to demonstrate how positional arguments work: 53 | 54 | ### Example 1: A Function with Two Parameters 55 | 56 | ```python 57 | def add_numbers(a, b): 58 | return a - b 59 | 60 | result = add_numbers(10, 5) 61 | print(result) # Output: 5 62 | ``` 63 | 64 | In this example, `10` is passed to the parameter `a` and `5` is passed to `b`. The difference of the two is returned. 65 | However, we the result will be changed if we pass the parameter in opposite order. Do check that out. So the order is crucial in positional arguments. 66 | 67 | ### Example 2: Function with Three Positional Arguments 68 | 69 | ```python 70 | def describe_person(name, age, city): 71 | print(f"{name} is {age} years old and lives in {city}.") 72 | 73 | describe_person("Musa", 25, "New York") 74 | # Output: Musa is 25 years old and lives in New York. 75 | ``` 76 | 77 | Here, the arguments are passed in the order corresponding to the parameters: `name`, `age`, and `city`. 78 | 79 | --- 80 | 81 | ## Multiple Positional Arguments 82 | 83 | We can pass any number of positional arguments to a function as long as they match the number of parameters defined in the function defination. 84 | 85 | ### Example 3: Function with Multiple Parameters 86 | 87 | ```python 88 | def multiply(x, y, z): 89 | return x * y * z 90 | 91 | result = multiply(2, 3, 4) 92 | print(result) # Output: 24 93 | ``` 94 | 95 | The function `multiply` accepts three positional arguments, and the result is the product of the three numbers. 96 | 97 | --- 98 | 99 | ## Best Practices for Positional Arguments 100 | 101 | - **Order Matters**: Since the arguments are mapped to parameters based on their position, always ensure they are passed in the correct order. 102 | 103 | - **Matching Parameters**: Ensure that the number of arguments matches the number of parameters in the function definition. If the numbers don’t match, Python will raise an error. 104 | 105 | ```python 106 | def subtract(a, b): 107 | return a - b 108 | 109 | # Correct usage 110 | print(subtract(10, 5)) # Output: 5 111 | 112 | # Incorrect usage 113 | # print(subtract(10)) # Error: subtract() missing 1 required positional argument: 'b' 114 | ``` 115 | 116 | - **Readability**: It’s essential to pass arguments in a way that makes the function call readable and understandable. 117 | 118 | --- 119 | 120 | ## Conclusion 121 | 122 | Positional arguments in Python allow us to pass values to a function in a straightforward manner, based on their position. Understanding how to use positional arguments effectively is crucial when writing clean, functional Python code. Just remember that order matters, and the number of arguments must match the function’s parameter list. 123 | 124 | For more advanced functionality, Python also supports keyword arguments, default arguments, and variable-length arguments (`*args` and `**kwargs`), but positional arguments are the foundation of function calls in Python. We will see `*args` and `**kwargs` in coming sections. 125 | 126 | --- 127 | 128 | -------------------------------------------------------------------------------- /08-functions/08d-args_kwargs/readme.md: -------------------------------------------------------------------------------- 1 | # `*args` and `**kwargs` in Python Functions 2 | 3 | Python functions allow for a wide range of flexibility when it comes to passing arguments. Two powerful features of Python are `*args` and `**kwargs`, which let us pass a variable number of arguments to a function. These features give developers more control and flexibility over how functions handle data. 4 | 5 | 6 | ## What are `*args` and `**kwargs`? 7 | 8 | - **`*args`**: Allows a function to accept any number of positional arguments as a tuple. 9 | - **`**kwargs`**: Allows a function to accept any number of keyword arguments as a dictionary. 10 | 11 | These features provide flexibility when we don’t know in advance how many arguments will be passed to our function. 12 | 13 | --- 14 | 15 | ## How `*args` Work 16 | 17 | When a function has `*args` as a parameter, it can accept any number of positional arguments. Inside the function, these arguments are available as a tuple. 18 | 19 | ### Syntax: 20 | ```python 21 | def function_name(*args): 22 | # args is a tuple containing all positional arguments 23 | for arg in args: 24 | print(arg) 25 | ``` 26 | 27 | ### Example: 28 | ```python 29 | def sum_numbers(*args): 30 | total = 0 31 | for num in args: 32 | total += num 33 | return total 34 | 35 | print(sum_numbers(1, 2, 3)) # Output: 6 36 | print(sum_numbers(4, 5, 6, 7)) # Output: 22 37 | ``` 38 | 39 | In this example, the function `sum_numbers` accepts any number of positional arguments and returns their sum. 40 | 41 | --- 42 | 43 | ## How `**kwargs` Work 44 | 45 | When a function has `**kwargs` as a parameter, it can accept any number of keyword arguments. Inside the function, these arguments are available as a dictionary where the keys are the argument names and the values are the corresponding values. 46 | 47 | ### Syntax: 48 | ```python 49 | def function_name(**kwargs): 50 | # kwargs is a dictionary containing all keyword arguments 51 | for key, value in kwargs.items(): 52 | print(f"{key}: {value}") 53 | ``` 54 | 55 | ### Example: 56 | ```python 57 | def print_info(**kwargs): 58 | for key, value in kwargs.items(): 59 | print(f"{key}: {value}") 60 | 61 | print_info(name="Ali", age=25, city="New York") 62 | # Output: 63 | # name: Ali 64 | # age: 25 65 | # city: New York 66 | ``` 67 | 68 | In this example, `print_info` accepts any number of keyword arguments and prints them. 69 | 70 | --- 71 | 72 | ## Examples of Using `*args` 73 | 74 | ### Example 1: A Function with Multiple Positional Arguments 75 | 76 | ```python 77 | def multiply_numbers(*args): 78 | result = 1 79 | for num in args: 80 | result *= num 81 | return result 82 | 83 | print(multiply_numbers(2, 3, 4)) # Output: 24 84 | print(multiply_numbers(1, 5, 10)) # Output: 50 85 | ``` 86 | 87 | In this function, `multiply_numbers` can accept a variable number of arguments and multiply them together. 88 | 89 | ### Example 2: Passing a List as `*args` 90 | 91 | We can also pass a list of arguments using the `*` operator. 92 | 93 | ```python 94 | numbers = [1, 2, 3, 4] 95 | print(multiply_numbers(*numbers)) # Output: 24 96 | ``` 97 | 98 | Here, the list `numbers` is unpacked into positional arguments using `*`. 99 | 100 | --- 101 | 102 | ## Examples of Using `**kwargs` 103 | 104 | ### Example 1: Function with Multiple Keyword Arguments 105 | 106 | ```python 107 | def greet(**kwargs): 108 | if 'name' in kwargs: 109 | print(f"Hello {kwargs['name']}!") 110 | else: 111 | print("Hello Guest!") 112 | 113 | greet(name="Bob") # Output: Hello Bob! 114 | greet() # Output: Hello Guest! 115 | ``` 116 | 117 | In this example, `greet` can accept any number of keyword arguments and checks if a `name` argument is provided. 118 | 119 | ### Example 2: Passing a Dictionary as `**kwargs` 120 | 121 | We can pass a dictionary of keyword arguments using the `**` operator. 122 | 123 | ```python 124 | person_info = {"name": "Ali", "age": 30} 125 | greet(**person_info) # Output: Hello Ali! 126 | ``` 127 | 128 | The dictionary `person_info` is unpacked and passed as keyword arguments to the `greet` function. 129 | 130 | --- 131 | 132 | ## Combining `*args` and `**kwargs` 133 | 134 | We can use both `*args` and `**kwargs` in the same function to handle a combination of positional and keyword arguments. 135 | 136 | ### Example 1: Handling Both Positional and Keyword Arguments 137 | 138 | ```python 139 | def display_info(*args, **kwargs): 140 | print("Positional arguments:", args) 141 | print("Keyword arguments:", kwargs) 142 | 143 | display_info(1, 2, 3, name="Ali", age=25) 144 | # Output: 145 | # Positional arguments: (1, 2, 3) 146 | # Keyword arguments: {'name': 'Ali', 'age': 25} 147 | ``` 148 | 149 | In this example, `display_info` accepts both positional and keyword arguments and prints them separately. 150 | 151 | ### Example 2: Function with Default Arguments, `*args`, and `**kwargs` 152 | 153 | ```python 154 | def order_pizza(size="medium", *toppings, **details): 155 | print(f"Size: {size}") 156 | print(f"Toppings: {', '.join(toppings)}") 157 | print("Details:") 158 | for key, value in details.items(): 159 | print(f" {key}: {value}") 160 | 161 | order_pizza("large", "pepperoni", "mushrooms", name="Ali", delivery_time="18:00") 162 | # Output: 163 | # Size: large 164 | # Toppings: pepperoni, mushrooms 165 | # Details: 166 | # name: Ali 167 | # delivery_time: 18:00 168 | ``` 169 | 170 | In this example, `order_pizza` accepts a default argument, multiple toppings as `*args`, and order details as `**kwargs`. 171 | 172 | --- 173 | 174 | ## Best Practices for `*args` and `**kwargs` 175 | 176 | 1. **Use `*args` for unknown number of positional arguments**: Use `*args` when you don’t know how many positional arguments will be passed to your function. 177 | 2. **Use `**kwargs` for optional keyword arguments**: Use `**kwargs` when you want to allow optional keyword arguments. 178 | 3. **Order matters**: When defining a function with both `*args` and `**kwargs`, the order should be: positional arguments, `*args`, default parameters, and then `**kwargs`. 179 | 4. **Descriptive variable names**: While `*args` and `**kwargs` are the convention, you can give them more descriptive names like `*numbers` or `**options` to improve readability. 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /08-functions/08e-keyword_arguments/readme.md: -------------------------------------------------------------------------------- 1 | # Keyword Arguments in Python Functions 2 | 3 | In Python, **keyword arguments**, allow us to pass arguments by explicitly specifying the parameter name. This is in contrast to **positional arguments**, where the position of the argument in the function call determines which parameter it will be assigned to. 4 | 5 | ## What Are Keyword Arguments? 6 | 7 | Keyword arguments are passed to a function using the syntax `parameter_name=value`. Unlike positional arguments, the order of the arguments doesn't matter, as each one is explicitly assigned to a parameter by name. 8 | 9 | ### Syntax: 10 | ```python 11 | def function_name(parameter1, parameter2, ...): 12 | # function body 13 | return something 14 | 15 | function_name(parameter1=value1, parameter2=value2, ...) 16 | ``` 17 | 18 | By using keyword arguments, you can make function calls more readable and flexible. 19 | 20 | --- 21 | 22 | ## How Keyword Arguments Work 23 | 24 | The following example illustrates a function that uses keyword arguments: 25 | 26 | ```python 27 | def greet(name, message): 28 | print(f"Hello {name}, {message}") 29 | 30 | greet(name="Ali", message="Welcome to Python!") # Output: Hello Ali, Welcome to Python! 31 | ``` 32 | 33 | In this case: 34 | - The argument `name` is assigned the value `"Ali"`. 35 | - The argument `message` is assigned `"Welcome to Python!"`. 36 | 37 | Even though the parameters were defined in a specific order in the function definition, we can pass the arguments in any order as long as the names match: 38 | 39 | ```python 40 | greet(message="Welcome to Python!", name="Ali") # Output: Hello Ali, Welcome to Python! 41 | ``` 42 | If we do not use keyword argument and change the position, our output will be changed. But if we use keyword argument, we can use any order. 43 | 44 | --- 45 | 46 | ## Examples of Keyword Arguments 47 | 48 | Here are some examples to demonstrate how keyword arguments can be used: 49 | 50 | ### Example 1: Using Keyword Arguments 51 | 52 | ```python 53 | def book_info(title, author, year): 54 | print(f"'{title}' by {author}, published in {year}") 55 | 56 | book_info(title="1984", author="George Orwell", year=1949) 57 | # Output: '1984' by George Orwell, published in 1949 58 | ``` 59 | 60 | ### Example 2: Using Default Parameter Values and Overriding with Keyword Arguments 61 | 62 | We can assign default values to parameters, and only provide values for those we want to override using keyword arguments. 63 | 64 | ```python 65 | def greet(name="Guest", message="Welcome!"): 66 | print(f"Hello {name}, {message}") 67 | 68 | 69 | greet() # Output: Hello Guest, Welcome! 70 | 71 | greet(name="Ali") # Output: Hello Ali, Welcome! 72 | 73 | greet(message="Good morning!") # Output: Hello Guest, Good morning! 74 | 75 | greet(name="Ali", message="Good morning!") # Output: Hello Ali, Good morning! 76 | ``` 77 | 78 | In this example: 79 | - If no arguments are passed, the default values `"Guest"` and `"Welcome!"` are used. 80 | 81 | - If a keyword argument is provided, it overrides the default. 82 | 83 | --- 84 | 85 | ## Using Positional and Keyword Arguments Together 86 | 87 | We can combine both positional and keyword arguments in a function call. However, there are a few important rules: 88 | 1. Positional arguments must come before keyword arguments. 89 | 2. Once we use a keyword argument, all subsequent arguments must also be keyword arguments. 90 | 91 | ### Example 3: Combining Positional and Keyword Arguments 92 | 93 | ```python 94 | def describe_pet(pet_name, animal_type="dog"): 95 | print(f"I have a {animal_type} named {pet_name}.") 96 | 97 | # Positional argument for pet_name, keyword argument for animal_type 98 | describe_pet("Tickle", animal_type="cat") 99 | # Output: I have a cat named Tickle. 100 | 101 | # Positional argument for both parameters 102 | describe_pet("Max", "dog") 103 | # Output: I have a dog named Max. 104 | 105 | # Keyword arguments for both 106 | describe_pet(animal_type="duck", pet_name="i forgot") 107 | # Output: I have a duck named i forgot. 108 | ``` 109 | 110 | ### Example 4: Mixed Positional and Keyword Arguments 111 | 112 | ```python 113 | def order_drink(drink, size="medium", sugar="regular"): 114 | print(f"Order: {size} {drink} with {sugar} sugar.") 115 | 116 | # Positional for 'drink', keyword for others 117 | order_drink("coffee", size="large", sugar="no") 118 | # Output: Order: large coffee with no sugar. 119 | ``` 120 | 121 | --- 122 | 123 | ## Best Practices for Keyword Arguments 124 | 125 | - **Improves Readability**: When calling functions with multiple arguments, use keyword arguments for clarity, especially if some arguments have default values that don’t need to be overridden. 126 | 127 | - **Use Defaults**: If a function has parameters with default values, make use of keyword arguments to override only the ones that need to be changed, and keep the rest as default. 128 | 129 | - **Combine with Positional Arguments**: Positional arguments should be used when the parameters are obvious (like the first few parameters of a function), and keyword arguments can be used for optional or less frequently changed parameters. 130 | 131 | ```python 132 | def make_pizza(size, topping="cheese", crust="thin"): 133 | print(f"{size} pizza with {topping} and {crust} crust") 134 | 135 | # Clear and readable 136 | make_pizza("large", crust="stuffed") 137 | # Output: large pizza with cheese and stuffed crust 138 | ``` 139 | 140 | 141 | -------------------------------------------------------------------------------- /08-functions/08f-scopes_in_python/readme.md: -------------------------------------------------------------------------------- 1 | # Scopes in Python 2 | The scope of variable in python is an environment where it can be recognizable. 3 | For example, the scope of parameter is always inside the function. 4 | 5 | **Example:** 6 | 7 | ```python 8 | def greetings(name): 9 | print(name) 10 | 11 | greetings("Rehan") 12 | ``` 13 | In above example, the scope of parameter `name` is only inside the function. If we try to use it outside the function, it will generate error. 14 | 15 | Let's try it. 16 | 17 | ```python 18 | def greetings(name): 19 | print(name) 20 | 21 | greetings("Rehan") 22 | print(name) # NameError: name 'name' is not defined 23 | ``` 24 | 25 | **The variable existing outside the function has a global scope. Means, it can be accessed inside the function.** 26 | 27 | **Example:** 28 | ```python 29 | my_name : str = "Musa" 30 | 31 | def greetings(): 32 | print(f"Hello {my_name}") 33 | 34 | greetings() # Output: Hello Musa 35 | ``` 36 | 37 | **What if variable existing outside the function has the same name as the parameter of the function?** 38 | 39 | ```python 40 | name : str = "Musa" 41 | def greetings(name): 42 | print(f"Name Inside Function: {name}") 43 | 44 | greetings("Rehan") 45 | print(f"Name Outside Function:{name}") 46 | 47 | ``` 48 | or 49 | 50 | ```python 51 | name : str = "Musa" 52 | def greetings(): 53 | name = "Rehan" 54 | print(f"Name Inside Function: {name}") 55 | 56 | greetings() 57 | print(f"Name Outside Function:{name}") 58 | 59 | ``` 60 | In above example, we are using same variable `name` inside and outside the function. The results conclude the followings. 61 | - Parameters doesn't exist outside the function. 62 | - If the name of the variable is same, the python is intelligent enough to differentiate it. 63 | - It will print the value provided as argument (parameter value) inside the function and outside the function, it will print the value of the variable exisiting outside the funciton. 64 | 65 | ## The `global` keyword 66 | We can declare a variable outside the function and access it inside the function with `global` keyword. 67 | 68 | ```python 69 | teacher_name : str = "Usman" 70 | 71 | def greetings(): 72 | global teacher_name 73 | teacher_name = "Rehan" 74 | print(f"Inside Function: {teacher_name}") 75 | 76 | greetings() 77 | 78 | print(f"Outside the Function: {teacher_name}") 79 | 80 | ``` 81 | 82 | ## Important Points 83 | ### For Immutable Types 84 | - Changing the parameter's value doesn't propagate outside the 85 | function (in any case, not when the variable is a scalar (primitive)). 86 | 87 | - This also means that a function receives the argument's value, not the argument itself. 88 | 89 | ```python 90 | def any_function (value): 91 | print(f"value Received: {value}") 92 | value += 100 93 | print(f"value changed inside func : {value}") 94 | 95 | value = 4 96 | any_function(value) 97 | print(f"Value outside the func: {value}") 98 | 99 | ``` 100 | ### For Mutable Types 101 | But for lists, a case is bit different. Let's learn from same example but instead we'll use lists. 102 | 103 | ```python 104 | def any_function (num_list): 105 | print(f"value Received: {num_list}") 106 | num_list.append(500) 107 | print(f"num_list changed inside func : {num_list}") 108 | 109 | num_list = [1, 2, 4, 5] 110 | any_function(num_list) 111 | print(f"num_list outside the func: {num_list}") 112 | ``` 113 | #### Explanation: 114 | - When a scalar is passed to a function, Python creates a copy of the value (since they are immutable). Any modifications inside the function will be applied to the local copy, and the original variable outside the function remains unchanged. 115 | 116 | - In contrast, if the function were modifying a mutable object like a list or dictionary, the changes would be reflected outside the function. This is because mutables are passed by reference, and their internal state can be altered without creating a new copy. -------------------------------------------------------------------------------- /08-functions/08g-recursive_functions/readme.md: -------------------------------------------------------------------------------- 1 | # Recursive Functions in Python 2 | 3 | Functions in Python are powerful tools for structuring programs, and **recursive functions** offer even more flexibility. 4 | 5 | ## What is Recursion? 6 | 7 | **Recursion** is a programming technique where a function calls itself to solve a problem. The key idea is that the problem is broken down into smaller sub-problems, and the function continues calling itself with simpler inputs until it reaches a base case (a stopping condition). 8 | 9 | ### Key Points: 10 | 11 | - Every recursive function has a **base case** to terminate the recursion. 12 | 13 | - A recursive function solves a problem by reducing it to a smaller version of itself. 14 | 15 | --- 16 | 17 | ## Examples of Recursive Functions 18 | 19 | ### Example 1: Factorial Calculation 20 | 21 | The **factorial** of a number `n` is the product of all positive integers less than or equal to `n`. Factorials can be defined recursively, where `n! = n * (n-1)!` and the base case is `0! = 1`. 22 | 23 | **Without Recursion** 24 | 25 | ```python 26 | def factorial(n): 27 | if n < 0: 28 | return None 29 | if n < 2: 30 | return 1 31 | else: 32 | result = 1 33 | for i in range(2,n+1): 34 | result *= i 35 | return result 36 | print(factorial(5)) # Output: 120 37 | 38 | ``` 39 | 40 | **With Recursion** 41 | 42 | ```python 43 | def factorial(n): 44 | # Base case 45 | if n < 0: 46 | return None 47 | if n < 2: 48 | return 1 49 | # Recursive case 50 | return n * factorial(n - 1) 51 | 52 | print(factorial(5)) # Output: 120 53 | ``` 54 | 55 | ### Example 2: Fibonacci Sequence 56 | 57 | The **Fibonacci sequence** is a series where each number is the sum of the two preceding ones. The recursive relation is `F(n) = F(n-1) + F(n-2)` with the base cases `F(0) = 0` and `F(1) = 1`. 58 | 59 | **Without Recursion** 60 | 61 | ```python 62 | def fibbonacci (n): 63 | if n < 0: 64 | return None 65 | if n < 3: 66 | return 1 67 | else: 68 | e1 = e2 = 1 69 | sum = 0 70 | for i in range(3, n+1): 71 | sum = e1 + e2 72 | e1, e2 = e2, sum 73 | return sum 74 | print (fibbonacci(6)) 75 | 76 | ``` 77 | 78 | **With Recursion** 79 | 80 | ```python 81 | def fibbonacci (n): 82 | if n < 0: 83 | return None 84 | if n < 3: 85 | return 1 86 | else: 87 | return fibbonacci(n-1) + fibbonacci(n-2) 88 | print (fibbonacci(6)) 89 | ``` 90 | 91 | ## Key Notes 92 | 93 | - If you forget to consider the conditions which can stop the 94 | chain of recursive invocations, the program may enter an infinite loop. 95 | - Recursive calls consume a lot of memory and therefore recursive functions may sometimes 96 | be inefficient. 97 | -------------------------------------------------------------------------------- /08-functions/08h- lambda_function/readme.md: -------------------------------------------------------------------------------- 1 | # Lambda Functions in Python 2 | 3 | In Python, **lambda functions** are anonymous, inline functions that can have any number of arguments but only a single expression. They are often used for simple operations and as a shorthand in situations where defining a full function would be unnecessary or cumbersome. 4 | 5 | ## What is a Lambda Function? 6 | 7 | A **lambda function** in Python is a small anonymous function defined with the `lambda` keyword. Unlike regular functions defined using `def`, a lambda function is a single expression. Lambda functions are often used as arguments to higher-order functions like `map()`, `filter()`, and `sorted()`. 8 | 9 | ### Key Points: 10 | 11 | - **Anonymous**: Lambda functions are not bound to a name, hence they're referred to as anonymous functions. 12 | - **Single expression**: They contain only one expression and automatically return the result of that expression. 13 | - **Inline**: They are often used inline and passed as arguments to functions. 14 | 15 | --- 16 | 17 | ## Syntax of a Lambda Function 18 | 19 | The syntax of a lambda function is straightforward: 20 | 21 | ```python 22 | lambda arguments: expression 23 | ``` 24 | 25 | - **`lambda`**: The keyword used to define a lambda function. 26 | - **`arguments`**: A comma-separated list of parameters (just like a normal function). 27 | - **`expression`**: A single expression that is evaluated and returned. 28 | 29 | ### Example of Lambda Syntax: 30 | 31 | ```python 32 | # A lambda function that adds 10 to a number 33 | add_ten = lambda x: x + 10 34 | 35 | # Call the lambda function 36 | print(add_ten(5)) # Output: 15 37 | ``` 38 | 39 | --- 40 | 41 | ## Examples of Lambda Functions 42 | 43 | ### Example 1: Basic Lambda Function 44 | 45 | A simple lambda function that takes one argument and returns its square. 46 | 47 | ```python 48 | # Lambda function to square a number 49 | square = lambda x: x * x 50 | 51 | print(square(4)) # Output: 16 52 | ``` 53 | 54 | ### Example 2: Lambda with Multiple Arguments 55 | 56 | Lambda functions can take multiple arguments just like regular functions. 57 | 58 | ```python 59 | # Lambda function to multiply two numbers 60 | multiply = lambda a, b: a * b 61 | 62 | print(multiply(3, 5)) # Output: 15 63 | ``` 64 | 65 | ### Example 3: Using Lambda with Built-in Functions 66 | 67 | Lambda functions are commonly used with Python’s built-in higher-order functions like `map()`, `filter()`, and `sorted()`. 68 | 69 | #### `map()` with Lambda: 70 | 71 | `map()` applies a lambda function to each item in a list or iterable. 72 | 73 | ```python 74 | numbers = [1, 2, 3, 4, 5] 75 | 76 | # Square each number using map and lambda 77 | squared_numbers = list(map(lambda x: x ** 2, numbers)) 78 | 79 | print(squared_numbers) # Output: [1, 4, 9, 16, 25] 80 | ``` 81 | 82 | #### `filter()` with Lambda: 83 | 84 | `filter()` returns elements of a list that satisfy a given condition (lambda function). 85 | 86 | ```python 87 | numbers = [1, 2, 3, 4, 5, 6] 88 | 89 | # Filter out even numbers 90 | even_numbers = list(filter(lambda x: x % 2 == 0, numbers)) 91 | 92 | print(even_numbers) # Output: [2, 4, 6] 93 | ``` 94 | 95 | #### `sorted()` with Lambda: 96 | 97 | `sorted()` can use a lambda function as a `key` to sort based on custom criteria. 98 | 99 | ```python 100 | students = [("Ali", 85), ("Zara", 90), ("Bob", 75)] 101 | 102 | # Sort by student scores (second item in tuple) 103 | sorted_students = sorted(students, key=lambda x: x[1]) 104 | 105 | print(sorted_students) # Output: [('Bob', 75), ('Ali', 85), ('Zara', 90)] 106 | ``` 107 | 108 | --- 109 | 110 | ## Use Cases for Lambda Functions 111 | 112 | 1. **Simple Operations**: Lambda functions are ideal for small, simple operations where defining a full `def` function would be unnecessary. 113 | 114 | Example: Applying a simple mathematical operation inline: 115 | 116 | ```python 117 | add_five = lambda x: x + 5 118 | print(add_five(10)) # Output: 15 119 | ``` 120 | 121 | 2. **Higher-Order Functions**: When working with functions like `map()`, `filter()`, and `reduce()`, lambda functions provide a concise and readable way to pass behavior to the function. 122 | 3. **Sorting**: Lambda functions are often used with the `sorted()` function to customize sorting based on specific fields. 123 | 124 | 4. **Functional Programming**: Lambdas are useful in functional programming patterns where functions are passed as arguments or returned from other functions. 125 | 126 | --- 127 | 128 | ## Limitations of Lambda Functions 129 | 130 | 1. **Single Expression**: Lambda functions are limited to one expression, which can sometimes make them less readable if complex logic is required. 131 | 2. **No Statements**: Unlike normal functions, lambda functions can't contain statements such as loops, print, or multi-line logic. 132 | 133 | 3. **Less Readable for Complex Operations**: While lambda functions are concise, they can reduce readability if overused or used for complex logic. 134 | 135 | 4. **No Annotations**: Lambda functions don't support function annotations (type hints). 136 | -------------------------------------------------------------------------------- /08-functions/readme.md: -------------------------------------------------------------------------------- 1 | # Functions in Python 2 | We've used functions many times, like `print()` and `input()`, to make tasks simpler. We've also worked with methods, which are special kinds of functions. Now, it's time to write our own functions, starting with simple ones and progressing to more complex examples. 3 | 4 | ## Why we need functions 5 | Often, we'll find the same code repeating in our program with minor changes. Copying and pasting might seem convenient, but if an error occurs, fixing it everywhere can be tedious and risky. This is where functions come in. When a piece of code is repeated in multiple places, consider turning it into a function to streamline our code. 6 | 7 | As programs grow, they can become difficult to manage. While comments help, too many make the code harder to follow. A well-written function should be concise and easily understood at a glance. Skilled developers break problems into small, isolated tasks, each handled by its own function, keeping the code clean and organized. 8 | 9 | ## Types of Functions 10 | 1. Built-in Functions: 11 | - We have already used python builtin functions e.g. `print()`, `input()`, `int()`, `float()`. 12 | 2. User defined Functions 13 | - User-defined functions are those functions which are defined by the user, for the user. 14 | 15 | ## When to create a function 16 | 1. When a particular fragment of the code begins to appear in more than one place, consider the possibility of isolating it in the form of a function invoked. 17 | 2. When a piece of code becomes so large that reading and understating it may cause a problem, consider dividing it into separate, smaller 18 | problems, and implement each of them in the form of a separate function. 19 | 3. Decompose the problem to allow the product to be implemented as a set of separately written functions packed together in different modules. 20 | 21 | **Example** 22 | ```python 23 | a : int = int(input("Enter a value")) 24 | print(a) 25 | 26 | b : int = int(input("Enter a value")) 27 | print(b) 28 | 29 | c : int = int(input("Enter a value")) 30 | print(c) 31 | 32 | ``` 33 | We have written above example to get a number from user and print that number. We are doing this 3 times. 34 | This code is absolutely fine and will work. But what if our client/teacher asks as to print the number in this form. "User gave the number ". 35 | So we have to change the print statement on multiple lines. 36 | 37 | In above example, we are repeating our code. We can use function here like so. 38 | ```python 39 | def print_number(): 40 | a : int = int(input("Enter a value")) 41 | print(a) 42 | ``` 43 | Now see, we have to change the print on just a single line. Let's chage it as per client/teacher requirement. 44 | ```python 45 | def print_number(): 46 | a : int = int(input("Enter a value")) 47 | print(f"User gave the number {a}") 48 | ``` 49 | 50 | ## How to create and use the function 51 | For functions, we have to first define a function, then invoke (use) the function wherever it is required. 52 | 53 | **Step-1: Define (Create) a function** 54 | Here is the syntax to create a function. 55 | - We start from keyword `def`. 56 | - Then we add function name. `def function_name`. The naming conventions for naming the functions is same as naming the variable in python. 57 | - Then we add paranthesis `def function_name()`. 58 | - And finally, we add colon `def function_name():` 59 | - After the colon, comes our function body. Function body contains the code/logic of the task function in bening written for. 60 | 61 | Now our funciton has been defined. We can use it wherever we require. 62 | 63 | **Step-2: Invoke (Use) a function** 64 | We can use our function like so 65 | ```python 66 | function_name() 67 | ``` -------------------------------------------------------------------------------- /09-OOP/09a-classes/README.md: -------------------------------------------------------------------------------- 1 | # Classes: 2 | Imagine we’re trying to categorize everything in the world into two main groups: 3 | 4 | Living Things: Things that are alive, like humans, animals, and plants. 5 | Non-Living Things: Objects that do not have life, like cars, rocks, and furniture. 6 | 7 | From there, we can further divide Living Things into: 8 | - Humans 9 | - Plants 10 | 11 | And within Humans, we can even have specific types, like: 12 | - Male 13 | - Female 14 | 15 | This is how we naturally organize things, and it’s very similar to how classes and objects work in programming. 16 | 17 | --- 18 | 19 | ## What are Classes in Python? 20 | 21 | In Python, a class allows you to create a blueprint for something (like a HUMAN). From this blueprint, you can create objects, each with its own attributes and methods. Think of a class as a category and objects as the actual instances that belong to that category. 22 | 23 | Key Terms: 24 | Class: A blueprint or template for creating objects. 25 | Object: An instance of a class. Each object has its own attributes and can perform actions. 26 | Attributes: The data or properties related to an object (e.g., a name, height or age). 27 | Methods: The actions or functions that an object can perform (e.g., a human can breathe, walk etc). 28 | 29 | 30 | ## How to write Class code 31 | 32 | ``` 33 | class Human: 34 | def __init__(self, name, age): 35 | self.name = name 36 | self.age = age 37 | 38 | def speak(self): 39 | print(f"{self.name} is speaking.") 40 | 41 | # Create an instance (object) of the class 42 | person = Human("Alice", 25) 43 | 44 | # Call the speak method 45 | person.speak() # Output: Alice is speaking. 46 | 47 | ``` 48 | 49 | ### Let’s break down the syntax of the Human class step by step: 50 | 51 | def __init__(self, name, age, gender): 52 | __init__ is a special method called a constructor. This method is automatically called when an object is created from the class. 53 | It initializes the object's attributes (or properties) — name, age, and gender in this case. 54 | 55 | **Parameters:** 56 | 57 | self: Refers to the instance (or object) of the class. It allows the object to reference its own attributes and methods. Every method in a class must have self as the first parameter. 58 | name, age, gender: These are the parameters passed to the constructor when creating an object of the Human class. 59 | 60 | ### self Must Be the First Parameter in Methods 61 | In Python, every method in a class must include self as the first parameter. This is because the method needs to know which instance of the class it is working with. However, when you call the method, you don't need to pass self explicitly — Python automatically does this behind the scenes. 62 | 63 | 64 | 65 | **def speak(self):** 66 | 67 | This defines a method named speak that is part of the Human class. Methods are functions that belong to a class and define actions the objects of the class can perform. 68 | The speak method allows the Human object to "speak" by printing a message. 69 | 70 | **person = Human("Alice", 25, "Female")** 71 | 72 | This creates an instance (or object) of the Human class. 73 | Human("Alice", 25, "Female") calls the __init__ method, passing "Alice", 25, and "Female" as arguments. These values will be used to initialize the attributes of the person object. 74 | After this line, the object person has the following attributes: 75 | 76 | person.name = "Alice" 77 | person.age = 25 78 | person.gender = "Female" 79 | 80 | --- 81 | 82 | **What Happens if You Use a Different Name for the Constructor other then __init__?** 83 | 84 | ``` 85 | class Human: 86 | def initialize(self, name, age): # Using a non-standard method name 87 | self.name = name 88 | self.age = age 89 | 90 | def speak(self): 91 | print(f"My name is {self.name}, and I am {self.age} years old.") 92 | 93 | # Create an instance of the Human class 94 | person = Human() # No arguments passed, because no __init__ method is called automatically 95 | 96 | # Call initialize method explicitly 97 | person.initialize("Alice", 25) # Now we must call this manually 98 | 99 | # Call the speak method 100 | person.speak() # Output: My name is Alice, and I am 25 years old. 101 | 102 | ``` 103 | 104 | --- 105 | **What if You use any Different name as the first parameter in a Constructor other then self** 106 | 107 | You can technically use any other name as the first parameter in a class method, and Python will still treat it as the instance of the object. However, it's highly recommended to stick to self for clarity and consistency. 108 | 109 | ``` 110 | class Human: 111 | def __init__(instance, name, age): # Using 'instance' instead of 'self' 112 | instance.name = name 113 | instance.age = age 114 | 115 | def speak(instance): # Using 'instance' instead of 'self' 116 | print(f"My name is {instance.name}, and I am {instance.age} years old.") 117 | ``` 118 | -------------------------------------------------------------------------------- /09-OOP/09b-Objects/README.md: -------------------------------------------------------------------------------- 1 | ## What Are Objects in Python? 2 | 3 | In Python, an object is an instance of a class. While a class is like a blueprint or template, an object is the actual thing created from that blueprint. Objects have both attributes (data or properties) and methods (functions or actions they can perform) defined by the class. 4 | 5 | ### How to Create Objects 6 | 7 | To create an object in Python, you simply call the class as if it were a function, passing the required arguments to its constructor (typically the __init__ method). The class then returns a new object (instance) of that class. 8 | 9 | ``` 10 | object_name = ClassName(arguments) 11 | 12 | ``` 13 | 14 | 15 | Example: 16 | Let's say we have a class Human: 17 | 18 | ``` 19 | class Human: 20 | def __init__(self, name, age): 21 | self.name = name 22 | self.age = age 23 | 24 | def speak(self): 25 | print(f"My name is {self.name} and I am {self.age} years old.") 26 | 27 | 28 | # Create an object 29 | person = Human("Alice", 25) 30 | 31 | # Access the object's methods and attributes 32 | person.speak() # Output: My name is Alice and I am 25 years old. 33 | 34 | ``` 35 | 36 | In this example: 37 | 38 | Human is the class. 39 | person is an object (or instance) of the Human class. 40 | "Alice" and 25 are passed as arguments to the __init__ constructor method, which sets the object's name and age attributes. 41 | 42 | 43 | -------------------------------------------------------------------------------- /09-OOP/09c-inheritance/code.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 4, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "['Rehan', 'Usman', 'Mazhar']\n", 13 | "\n", 14 | "[]\n" 15 | ] 16 | } 17 | ], 18 | "source": [ 19 | "names = [\"Rehan\", \"Usman\", \"Mazhar\"]\n", 20 | "print(names)\n", 21 | "print(type(names))\n", 22 | "names.append(\"Musa\")\n", 23 | "my_name = list()\n", 24 | "print(my_name)" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 25, 30 | "metadata": {}, 31 | "outputs": [ 32 | { 33 | "name": "stdout", 34 | "output_type": "stream", 35 | "text": [ 36 | "<__main__.Person object at 0x000001C39C8E5070>\n", 37 | "{'cname': 'Rehan'}\n", 38 | "<__main__.Person object at 0x000001C39C8E48F0>\n", 39 | "{'cname': 'Usman'}\n" 40 | ] 41 | } 42 | ], 43 | "source": [ 44 | "# attributes/variables\n", 45 | "# methods/functions\n", 46 | "\n", 47 | "class Person:\n", 48 | " def __init__(self,name):\n", 49 | " self.cname = name\n", 50 | "\n", 51 | "new_person = Person(\"Rehan\")\n", 52 | "print(new_person)\n", 53 | "print(new_person.__dict__)\n", 54 | "\n", 55 | "another_person = Person(\"Usman\")\n", 56 | "print(another_person)\n", 57 | "print(another_person.__dict__)" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 14, 63 | "metadata": {}, 64 | "outputs": [ 65 | { 66 | "name": "stdout", 67 | "output_type": "stream", 68 | "text": [ 69 | "My name is Rehan\n", 70 | "Rehan\n", 71 | "My name is Usman\n", 72 | "Usman\n" 73 | ] 74 | } 75 | ], 76 | "source": [ 77 | "# attributes/variables\n", 78 | "# methods/functions\n", 79 | "\n", 80 | "class Person:\n", 81 | " def __init__(self,name):\n", 82 | " self.cname = name\n", 83 | " \n", 84 | " def display_info(self):\n", 85 | " print(f\"My name is {self.cname}\")\n", 86 | "\n", 87 | "\n", 88 | "new_person = Person(\"Rehan\")\n", 89 | "new_person.display_info()\n", 90 | "print(new_person.cname)\n", 91 | "\n", 92 | "\n", 93 | "another_person = Person(\"Usman\")\n", 94 | "another_person.display_info()\n", 95 | "print(another_person.cname\n", 96 | ")\n" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": 22, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "# Parent class\n", 106 | "class User:\n", 107 | " def __init__(self, username, email):\n", 108 | " self.username = username\n", 109 | " self.email = email\n", 110 | " print(\"Parent constructor called\")\n", 111 | "\n", 112 | " def display_user_info(self):\n", 113 | " print(f\"User: {self.username}, Email: {self.email}\")" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": 24, 119 | "metadata": {}, 120 | "outputs": [ 121 | { 122 | "name": "stdout", 123 | "output_type": "stream", 124 | "text": [ 125 | "Child constructor called\n", 126 | "Parent constructor called\n", 127 | "{'access_level': 'fullaccess', 'username': 'Rehan', 'email': 'example@1232'}\n" 128 | ] 129 | } 130 | ], 131 | "source": [ 132 | "class Admin(User):\n", 133 | " def __init__(self,username, email, access_level):\n", 134 | " self.access_level = access_level\n", 135 | " print(\"Child constructor called\")\n", 136 | " super().__init__(username, email)\n", 137 | "\n", 138 | "\n", 139 | "user01 = Admin(\"Rehan\", \"example@1232\", \"fullaccess\")\n", 140 | "\n", 141 | "print(user01.__dict__)" 142 | ] 143 | } 144 | ], 145 | "metadata": { 146 | "kernelspec": { 147 | "display_name": "Python 3", 148 | "language": "python", 149 | "name": "python3" 150 | }, 151 | "language_info": { 152 | "codemirror_mode": { 153 | "name": "ipython", 154 | "version": 3 155 | }, 156 | "file_extension": ".py", 157 | "mimetype": "text/x-python", 158 | "name": "python", 159 | "nbconvert_exporter": "python", 160 | "pygments_lexer": "ipython3", 161 | "version": "3.12.2" 162 | } 163 | }, 164 | "nbformat": 4, 165 | "nbformat_minor": 2 166 | } 167 | -------------------------------------------------------------------------------- /09-OOP/09d-abstraction/readme.md: -------------------------------------------------------------------------------- 1 | # **Abstraction, Abstract Classes, and Abstract Methods in Python** 2 | 3 | ## **1. Introduction to Abstraction in Python OOP** 4 | 5 | ### **What is Abstraction?** 6 | In Object-Oriented Programming (OOP), **abstraction** is the concept of **hiding the internal details** of an object and **exposing only the necessary information**. It allows you to work with higher-level concepts without getting bogged down by the details of how something works. 7 | 8 | Think of a **car**: 9 | - You know that you need to press the accelerator to move faster, but you don’t need to know how the engine works to achieve that. That’s abstraction — you only need to know what to do, not how it happens. 10 | 11 | In Python, abstraction is typically achieved through **abstract classes** and **abstract methods**. 12 | 13 | ### **When to Use Abstraction?** 14 | - When you need to define a common **interface** for multiple related objects (like defining how all types of shapes must have a method to calculate their area). 15 | - When you want to **hide complexity** from users and provide them with a simplified way to interact with objects. 16 | - When you want to **enforce** that certain methods should be implemented by all subclasses, but you don’t want to provide a specific implementation in the base class. 17 | 18 | --- 19 | 20 | ## **2. Abstract Classes in Python** 21 | 22 | ### **What is an Abstract Class?** 23 | An **abstract class** in Python is a class that **cannot be instantiated** directly. It serves as a **blueprint** for other classes. Abstract classes may contain abstract methods that **must** be implemented by any subclass that inherits from the abstract class. 24 | 25 | In Python, abstract classes are defined using the `abc` module (Abstract Base Classes). 26 | 27 | ### **Why Use Abstract Classes?** 28 | - To **enforce a contract**: If you want to ensure that all subclasses implement certain methods, you can define those methods as abstract in the base class. 29 | - To provide **partial implementation**: You can define common methods in the abstract class and leave specific details to the subclasses. 30 | 31 | ### **How to Create an Abstract Class?** 32 | 1. Import `ABC` (Abstract Base Class) from the `abc` module. 33 | 2. Inherit from `ABC` in your base class. 34 | 3. Use the `@abstractmethod` decorator to mark methods that must be implemented by subclasses. 35 | 36 | --- 37 | 38 | ## **3. Abstract Methods in Python** 39 | 40 | ### **What is an Abstract Method?** 41 | An **abstract method** is a method that is **declared but contains no implementation**. Subclasses of an abstract class **must provide an implementation** for the abstract methods. Abstract methods are defined using the `@abstractmethod` decorator. 42 | 43 | ### **When to Use Abstract Methods?** 44 | - When you want to **force subclasses** to implement a particular method. 45 | - When you need to define methods in the base class that have **no meaningful default behavior**, and it’s up to the subclasses to define them. 46 | 47 | --- 48 | 49 | ## **4. Example of Abstraction, Abstract Classes, and Abstract Methods** 50 | 51 | Here’s a simple example that shows how to use abstraction, abstract classes, and abstract methods in Python. 52 | 53 | ### **Example: Shape Class with Different Types of Shapes** 54 | 55 | ```python 56 | from abc import ABC, abstractmethod 57 | 58 | # Abstract class representing the concept of a shape 59 | class Shape(ABC): 60 | 61 | # Abstract method to calculate area 62 | @abstractmethod 63 | def area(self): 64 | pass 65 | 66 | # Abstract method to calculate perimeter 67 | @abstractmethod 68 | def perimeter(self): 69 | pass 70 | 71 | # Common method in the abstract class 72 | def description(self): 73 | return "This is a shape object" 74 | 75 | # Subclass for a Rectangle 76 | class Rectangle(Shape): 77 | 78 | def __init__(self, length, width): 79 | self.length = length 80 | self.width = width 81 | 82 | # Implementing the abstract method 'area' 83 | def area(self): 84 | return self.length * self.width 85 | 86 | # Implementing the abstract method 'perimeter' 87 | def perimeter(self): 88 | return 2 * (self.length + self.width) 89 | 90 | # Subclass for a Circle 91 | class Circle(Shape): 92 | 93 | def __init__(self, radius): 94 | self.radius = radius 95 | 96 | # Implementing the abstract method 'area' 97 | def area(self): 98 | return 3.1416 * self.radius * self.radius 99 | 100 | # Implementing the abstract method 'perimeter' 101 | def perimeter(self): 102 | return 2 * 3.1416 * self.radius 103 | 104 | # Instantiate the subclasses 105 | rect = Rectangle(5, 3) 106 | circle = Circle(4) 107 | 108 | # Access the abstract methods 109 | print(f"Rectangle area: {rect.area()}") 110 | print(f"Rectangle perimeter: {rect.perimeter()}") 111 | print(f"Circle area: {circle.area()}") 112 | print(f"Circle perimeter: {circle.perimeter()}") 113 | 114 | # Access the common method from the abstract class 115 | print(rect.description()) 116 | print(circle.description()) 117 | ``` 118 | 119 | ### **Explanation of the Example:** 120 | 1. **Shape Class**: 121 | - This is an abstract class because it inherits from `ABC`. 122 | - It contains two abstract methods: `area()` and `perimeter()`, which **must** be implemented by any subclass. 123 | 124 | 2. **Rectangle and Circle**: 125 | - These are concrete classes that **inherit** from `Shape`. 126 | - They **implement** the abstract methods `area()` and `perimeter()`. 127 | - These subclasses provide specific implementations for calculating the area and perimeter of rectangles and circles. 128 | 129 | 3. **Common Behavior**: 130 | - Both `Rectangle` and `Circle` inherit the `description()` method from the `Shape` class, which is **not abstract**, and therefore provides a common behavior for all shapes. 131 | 132 | ### **Key Points:** 133 | - You **cannot instantiate** the `Shape` class directly (e.g., `Shape()` would raise an error). 134 | - The subclasses **must** implement all the abstract methods from the `Shape` class, or they will also be abstract. 135 | - **Abstraction** here is the concept of working with "shapes" without needing to know the specifics of whether it’s a rectangle or circle, as both implement the methods required by the abstract class. 136 | 137 | --- 138 | 139 | ## **5. Real-World Use Cases of Abstraction** 140 | 141 | - **Frameworks**: Abstract classes are often used in frameworks where base functionality is defined, and the users of the framework must implement specific details. For example, GUI frameworks may have an abstract class `Window` with abstract methods like `draw()`, which concrete window classes must implement. 142 | 143 | - **Game Development**: You might have an abstract class `GameCharacter` that defines abstract methods like `attack()`, `defend()`, and `move()`. Each specific character (e.g., `Warrior`, `Mage`, `Archer`) must implement these methods differently. 144 | 145 | --- 146 | 147 | ## **6. Key Takeaways** 148 | 149 | - **Abstraction** is about **hiding complexity** and exposing only what’s necessary. 150 | - **Abstract classes** serve as blueprints for creating concrete subclasses. They cannot be instantiated directly. 151 | - **Abstract methods** are declared in abstract classes but must be implemented by any subclass. 152 | - Abstraction and abstract classes allow for the creation of a **flexible and scalable code structure** where specific details are handled by subclasses. 153 | -------------------------------------------------------------------------------- /09-OOP/09f-class_level_att_methods/readme.md: -------------------------------------------------------------------------------- 1 | # Python OOP: Class Variables, Class Methods, and Static Methods 2 | 3 | ## 1. **Class Variables** 4 | 5 | ### What Are Class Variables? 6 | - **Class variables** are variables that are shared among all instances of a class. 7 | - They are defined within the class but outside any methods. 8 | - All instances of the class have access to the same class variable. 9 | 10 | ### When to Use Class Variables? 11 | - Use class variables when you want to store data that should be shared across all instances of the class. 12 | - Commonly used for **constants** or **counters** that track information related to all objects of the class. 13 | 14 | ### Example of Class Variables: 15 | 16 | ```python 17 | class Car: 18 | # Class variable 19 | number_of_wheels = 4 20 | 21 | def __init__(self, model): 22 | # Instance variable 23 | self.model = model 24 | 25 | # Accessing class variable 26 | print(Car.number_of_wheels) # Output: 4 27 | 28 | # Creating instances 29 | car1 = Car("Toyota") 30 | car2 = Car("Honda") 31 | 32 | # Both instances share the same class variable 33 | print(car1.number_of_wheels) # Output: 4 34 | print(car2.number_of_wheels) # Output: 4 35 | 36 | # Modifying class variable through class 37 | Car.number_of_wheels = 3 38 | print(car1.number_of_wheels) # Output: 3 39 | ``` 40 | 41 | ### Key Points: 42 | - Class variables are shared among all instances of a class. 43 | - Changing a class variable affects all instances. 44 | 45 | --- 46 | 47 | ## 2. **Class Methods** 48 | 49 | ### What Are Class Methods? 50 | - **Class methods** are methods that are bound to the class, not the instance. 51 | - They can access and modify class variables. 52 | - A class method is defined using the `@classmethod` decorator, and the first parameter is `cls` (the class itself). 53 | 54 | ### When to Use Class Methods? 55 | - Use class methods when you need to modify class-level data (like class variables). 56 | - Class methods are often used to create factory methods that instantiate objects in a certain way. 57 | 58 | ### Example of Class Methods: 59 | 60 | ```python 61 | class Car: 62 | number_of_cars = 0 # Class variable 63 | 64 | def __init__(self, model): 65 | self.model = model 66 | Car.number_of_cars += 1 # Modify class variable in the constructor 67 | 68 | @classmethod 69 | def get_number_of_cars(cls): 70 | return f"Total cars: {cls.number_of_cars}" 71 | 72 | # Creating instances 73 | car1 = Car("Toyota") 74 | car2 = Car("Honda") 75 | 76 | # Using the class method to access class variable 77 | print(Car.get_number_of_cars()) # Output: Total cars: 2 78 | ``` 79 | 80 | ### Key Points: 81 | - Class methods can modify class variables and are called on the class, not on instances. 82 | - They are useful when you need to perform actions that affect the class itself rather than individual instances. 83 | 84 | --- 85 | 86 | ## 3. **Static Methods** 87 | 88 | ### What Are Static Methods? 89 | - **Static methods** are methods that do not depend on class or instance data. 90 | - They are defined using the `@staticmethod` decorator. 91 | - Static methods do not have access to `cls` (class) or `self` (instance). They behave just like regular functions but are included within a class for logical grouping. 92 | 93 | ### When to Use Static Methods? 94 | - Use static methods when you want a function that logically belongs to a class but does not need to access or modify class/instance-specific data. 95 | - Good for utility functions that operate independently of class or instance data. 96 | 97 | ### Example of Static Methods: 98 | 99 | ```python 100 | class MathOperations: 101 | 102 | @staticmethod 103 | def add(a, b): 104 | return a + b 105 | 106 | @staticmethod 107 | def subtract(a, b): 108 | return a - b 109 | 110 | # Using static methods without creating an instance 111 | print(MathOperations.add(5, 3)) # Output: 8 112 | print(MathOperations.subtract(10, 4)) # Output: 6 113 | ``` 114 | 115 | ### Key Points: 116 | - Static methods are independent of class and instance. 117 | - They are typically used for utility or helper functions related to the class. 118 | 119 | --- 120 | 121 | ## Summary: When to Use Each Concept 122 | 123 | | Concept | When to Use | 124 | |----------------|------------------------------------------------------------------------------------------------------| 125 | | **Class Variables** | When you want to store data that is shared among all instances (e.g., counters, constants). | 126 | | **Class Methods** | When you need to access or modify class-level data or want a method that logically affects the entire class. | 127 | | **Static Methods** | When you need a utility function that doesn’t require access to class or instance-specific data. | 128 | 129 | --- 130 | 131 | ## Example: Using All Concepts Together 132 | 133 | ```python 134 | class Car: 135 | number_of_cars = 0 # Class variable 136 | 137 | def __init__(self, model): 138 | self.model = model 139 | Car.number_of_cars += 1 # Increment class variable 140 | 141 | @classmethod 142 | def get_number_of_cars(cls): 143 | return f"Total cars: {cls.number_of_cars}" 144 | 145 | @staticmethod 146 | def car_type(): 147 | return "Four-wheeled vehicle" 148 | 149 | # Creating instances 150 | car1 = Car("Toyota") 151 | car2 = Car("Honda") 152 | 153 | # Accessing class variable using class method 154 | print(Car.get_number_of_cars()) # Output: Total cars: 2 155 | 156 | # Using static method 157 | print(Car.car_type()) # Output: Four-wheeled vehicle 158 | ``` 159 | 160 | ### Breakdown: 161 | - **Class Variable:** `number_of_cars` tracks the total number of car objects created. 162 | - **Class Method:** `get_number_of_cars` allows us to access the class variable and get the total count of cars. 163 | - **Static Method:** `car_type` is a utility method that returns a description of a car but does not interact with any instance or class-level data. 164 | 165 | 166 | -------------------------------------------------------------------------------- /09-OOP/09g-composition/readme.md: -------------------------------------------------------------------------------- 1 | # Composition in Python (OOP) 2 | 3 | ## 1. **What is Composition in Object-Oriented Programming (OOP)?** 4 | - **Composition** is a concept in OOP where one class is composed of one or more objects from other classes. 5 | - It describes a **"has-a" relationship** between classes. 6 | - Example: A **Car** "has-an" **Engine**, or a **Library** "has-many" **Books**. 7 | 8 | ## 2. **Difference Between Inheritance and Composition** 9 | - **Inheritance** represents an "is-a" relationship. 10 | - Example: A **Dog** "is-a" **Animal**. 11 | - **Composition** represents a "has-a" relationship. 12 | - Example: A **Car** "has-an" **Engine**. 13 | 14 | | Inheritance | Composition | 15 | |-------------|-------------| 16 | | "is-a" relationship | "has-a" relationship | 17 | | Parent-child structure | Whole-part structure | 18 | | A subclass inherits behavior from a parent class | A class contains objects of other classes to perform part of its functionality | 19 | 20 | ## 3. **Why Use Composition?** 21 | - **Reusability:** Instead of repeating code, you can reuse components (classes) in different objects. 22 | - **Separation of Concerns:** Each class is responsible for a single part of the functionality, making the code easier to maintain and test. 23 | - **Flexible Code:** You can change the components (parts) without affecting the whole object. For example, you can switch engines in a car without changing the entire car design. 24 | 25 | ## 4. **Example: Composition in Python** 26 | 27 | ```python 28 | class Engine: 29 | def __init__(self, horsepower): 30 | self.horsepower = horsepower 31 | 32 | def start(self): 33 | print(f"Engine with {self.horsepower} horsepower starts.") 34 | 35 | class Car: 36 | def __init__(self, model, horsepower): 37 | # Composition: Car "has-an" Engine 38 | self.model = model 39 | self.engine = Engine(horsepower) 40 | 41 | def drive(self): 42 | self.engine.start() # Using the Engine to start the car 43 | print(f"{self.model} is now driving.") 44 | 45 | 46 | my_car = Car("Toyota Camry", 268) 47 | my_car.drive() 48 | ``` 49 | 50 | **Explanation:** 51 | - The `Car` class is composed of an `Engine` object. 52 | - The `Car` class doesn't inherit from `Engine`, but uses it to perform its functionality. 53 | - This is a flexible and modular design. If you need to change the type of `Engine`, you only modify the `Engine` class without touching the `Car` class. 54 | 55 | ## 5. **When and Where to Use Composition** 56 | - **Use Composition** when: 57 | 1. You want to model a "has-a" relationship. 58 | 2. You need **modular components**. For example, if you have a class for `Engine` in a car, you can reuse it in other classes like `Motorcycle` or `Truck`. 59 | 3. You want to **separate concerns**. Each class can focus on one task (like `Engine` handles starting, while `Car` handles driving). 60 | 4. You want to create more **flexible designs**. You can change the internal components without breaking the system (e.g., replacing the engine in a car). 61 | 62 | - **Avoid Composition** if: 63 | 1. You need to model a natural "is-a" relationship (like **Dog** and **Animal**). In such cases, use inheritance. 64 | 2. The component class (`Engine`, in this case) isn’t truly a part of the whole object. 65 | 66 | ## 6. **Advantages of Composition** 67 | - **Better Modularity**: Each part of the system (like the engine in a car) is encapsulated in its own class. 68 | - **Easier Maintenance**: Smaller, specialized classes are easier to maintain, extend, and debug. 69 | - **Loose Coupling**: Objects are loosely connected, making them more adaptable to changes. You can swap one part without affecting the whole system. 70 | 71 | ## 7. **Advanced Composition Concepts** 72 | - **Multiple Compositions**: A class can be composed of more than one other class. 73 | - Example: A `Car` might have both `Engine` and `Wheel` objects. 74 | 75 | ```python 76 | class Wheel: 77 | def __init__(self, size): 78 | self.size = size 79 | 80 | class Car: 81 | def __init__(self, model, horsepower, wheel_size): 82 | self.model = model 83 | self.engine = Engine(horsepower) # Car has-an Engine 84 | self.wheels = Wheel(wheel_size) # Car has four Wheels 85 | 86 | def drive(self): 87 | self.engine.start() 88 | print(f"{self.model} with {self.wheels}-inch wheels is now driving.") 89 | 90 | my_car = Car("Ford Mustang", 450, 19) 91 | my_car.drive() 92 | ``` 93 | 94 | In this case, the car "has-an" engine and "has wheels," showcasing how multiple compositions can work together in a class. 95 | 96 | 97 | ## 8. **Another way to use Composition** 98 | ```python 99 | class FloorDivision(): 100 | def division(self, dividend, divisor): 101 | print(f"Floor division {dividend // divisor}") 102 | 103 | class SimpleDivision(): 104 | def division(self, dividend, divisor): 105 | print(f"Simple Division {dividend / divisor}") 106 | 107 | class Division(): 108 | def __init__(self, division_decider): 109 | self.division_decider = division_decider 110 | 111 | def divide(self, dividend, divisor): 112 | self.division_decider.division(dividend, divisor) 113 | 114 | # Create instances separately 115 | a = FloorDivision() 116 | b = SimpleDivision() 117 | 118 | # Pass FloorDivison instance in Division Class 119 | c = Division(a) 120 | c.divide(11,4) 121 | 122 | # Pass SimpleDivison instance in Division Class 123 | d = Division(b) 124 | d.divide(11,4) 125 | 126 | # Create the instances of Division Class directly 127 | floor_division = Division(FloorDivision()) 128 | simple_division = Division(SimpleDivision()) 129 | 130 | # Performing division operations 131 | floor_division.divide(11, 5) 132 | simple_division.divide(11, 5) 133 | ``` 134 | **Key Concepts:** 135 | * Polymorphism: This example also demonstrates the concept of polymorphism, where the Division class can work with different types of division (floor or simple) depending on the object passed to it. 136 | 137 | * Encapsulation: The division logic (either floor or simple) is encapsulated inside individual classes (FloorDivision and SimpleDivision). The Division class itself encapsulates the decision logic for which type of division to use. 138 | 139 | * Abstraction: The Division class provides a simplified interface (divide()), hiding the complex logic of choosing between floor division and simple division. This way, the user only interacts with a simple, abstracted method without needing to know the internal details 140 | -------------------------------------------------------------------------------- /09-OOP/README.md: -------------------------------------------------------------------------------- 1 | ## Understanding Procedural Programming and Object-Oriented Programming (OOP) 2 | 3 | When you start learning to code, you’ve likely started with Procedural Programming write programs using functions, loops, and put everything (data, logic, functions) in a single file or in just a few files without much structure, we often refer to it as a spaghetti code or Procedural programming (if it's messy and hard to follow). 4 | 5 | ### What is Procedural Programming? 6 | 7 | Procedural programming is the approach most beginners take when they first learn to code. It focuses on writing step-by-step instructions for the computer to follow. In this style, your program is essentially a series of commands and functions that perform actions on data. 8 | 9 | **Key Features of Procedural Programming:** 10 | 11 | - Functions: The main building blocks are functions. Each function performs a specific task, like adding two numbers, printing a message, or looping through data. 12 | - Sequential Execution: The program runs in a top-down manner, following the order of functions and instructions. 13 | - Data and Functions Are Separate: Data (like variables) and the functions that act on that data are kept separate. This means you define variables first and then create functions that manipulate these variables. 14 | 15 | 16 | ### Issues with Procedural Programming: 17 | - Hard to Manage Large Programs: As your program grows, keeping track of all the functions and variables becomes difficult. You might end up with many functions doing similar things, leading to confusion. 18 | 19 | - Code Duplication: If you need the same functionality for different parts of your program, you often end up copying and pasting code, which makes it hard to maintain. 20 | 21 | - No Data Encapsulation: In procedural programming, data is accessible from anywhere, making it easier to accidentally modify variables, leading to bugs. 22 | 23 | - Poor Scalability: When you want to add new features, you need to modify the existing functions and variables, which can lead to breaking other parts of the program. 24 | 25 | --- 26 | 27 | ## Object-Oriented Programming (OOP) 28 | 29 | As programs become more complex, Object-Oriented Programming (OOP) becomes essential. OOP is a way of organizing code that groups related data and functions together into objects. This allows you to manage larger programs more effectively, reducing duplication and improving scalability. 30 | 31 | ### Key Features of OOP: 32 | - Classes and Objects: In OOP, you define a blueprint called a class, and from this blueprint, you can create multiple objects. Objects are instances of classes and hold both data and functions related to that data. 33 | - Encapsulation: OOP bundles data (called attributes) and functions (called methods) together in an object, making it easier to manage and protect your data. 34 | - Reusability: Through inheritance, you can create new classes based on existing ones, reducing code duplication. 35 | - Scalability: OOP makes it easier to add new features without breaking existing code. 36 | 37 | 38 | 39 | ### Differences Between Procedural Programming and OOP 40 | 41 | | Aspect | Procedural Programming | Object-Oriented Programming (OOP) | 42 | |----------------------------|------------------------------------------|------------------------------------------| 43 | | **Basic Unit** | Functions and Procedures | Classes and Objects | 44 | | **Data and Functions** | Data and functions are separate | Data and functions are bundled in objects | 45 | | **Code Reusability** | Limited code reusability | High reusability through classes and inheritance | 46 | | **Encapsulation** | No encapsulation, data is exposed | Data is encapsulated and protected | 47 | | **Complexity Management** | Harder to manage as programs grow | Easier to manage large programs with classes | 48 | | **Code Duplication** | Often leads to code duplication | Reduces code duplication through inheritance and polymorphism | 49 | | **Scalability** | Not easily scalable for large programs | Easily scalable by adding new classes or modifying existing ones | 50 | -------------------------------------------------------------------------------- /10-version_contol_git/readme.md: -------------------------------------------------------------------------------- 1 | # Introduction to Git and GitHub 2 | 3 | ## Table of Contents 4 | 5 | 1. [What is Version Control?](#what-is-version-control) 6 | 2. [Introduction to Git](#introduction-to-git) 7 | - [What is Git?](#what-is-git) 8 | - [Why do we need Git?](#why-do-we-need-git) 9 | 3. [Basic Git Concepts](#basic-git-concepts) 10 | - [Repository](#repository) 11 | - [Commit](#commit) 12 | - [Branch](#branch) 13 | 4. [Getting Started with Git](#getting-started-with-git) 14 | - [Installing Git](#installing-git) 15 | - [Configuring Git](#configuring-git) 16 | 5. [Basic Git Commands](#basic-git-commands) 17 | - [git init](#git-init) 18 | - [git add](#git-add) 19 | - [git commit](#git-commit) 20 | - [git status](#git-status) 21 | - [git log](#git-log) 22 | 6. [Working with Branches](#working-with-branches) 23 | - [Creating a branch](#creating-a-branch) 24 | - [Switching branches](#switching-branches) 25 | - [Merging branches](#merging-branches) 26 | 7. [Introduction to GitHub](#introduction-to-github) 27 | - [What is GitHub?](#what-is-github) 28 | - [Why use GitHub?](#why-use-github) 29 | 8. [GitHub Basics](#github-basics) 30 | - [Creating a GitHub account](#creating-a-github-account) 31 | - [Creating a repository on GitHub](#creating-a-repository-on-github) 32 | - [Cloning a repository](#cloning-a-repository) 33 | 9. [Collaborating with GitHub](#collaborating-with-github) 34 | - [Forking a repository](#forking-a-repository) 35 | - [Creating a Pull Request](#creating-a-pull-request) 36 | 10. [Best Practices and Tips](#best-practices-and-tips) 37 | 38 | ## What is Version Control? 39 | 40 | Version control is a system that helps track changes to files over time. It allows you to review changes, revert to previous versions, and collaborate with others more efficiently. Git is one of the most popular version control systems. 41 | 42 | ## Introduction to Git 43 | 44 | ### What is Git? 45 | 46 | Git is a distributed version control system designed to handle everything from small to very large projects with speed and efficiency. It was created by Linus Torvalds in 2005 for development of the Linux kernel. 47 | 48 | ### Why do we need Git? 49 | 50 | 1. **Track changes**: Git allows you to see who made changes to what and when. 51 | 2. **Revert to previous states**: If you make a mistake, you can easily roll back to a previous version. 52 | 3. **Collaborate**: Multiple people can work on the same project without interfering with each other's work. 53 | 4. **Backup**: Your code is stored on multiple computers, reducing the risk of losing work. 54 | 5. **Branching and merging**: Experiment with new features without affecting the main codebase. 55 | 56 | ## Basic Git Concepts 57 | 58 | ### Repository 59 | 60 | A repository (or "repo") is like a project's folder. It contains all of your project's files and stores each file's revision history. Repositories can exist either locally on your computer or as a remote copy on another computer. 61 | 62 | ### Commit 63 | 64 | A commit represents a specific point in your project's history. It's like a snapshot of your entire repository at a specific time. Each commit has a unique ID and includes a message describing the changes made. 65 | 66 | ### Branch 67 | 68 | A branch is an independent line of development. It allows you to work on different features or experiments without affecting the main codebase. The default branch is usually called "master" or "main". 69 | 70 | ## Getting Started with Git 71 | 72 | ### Installing Git 73 | 74 | - **Windows**: Download and install from [git-scm.com](https://git-scm.com/) 75 | - **macOS**: Install using Homebrew: `brew install git` 76 | - **Linux**: Use your distribution's package manager, e.g., `sudo apt-get install git` 77 | 78 | ### Configuring Git 79 | 80 | After installation, set up your name and email: 81 | 82 | ```bash 83 | git config --global user.name "Your Name" 84 | git config --global user.email "youremail@example.com" 85 | ``` 86 | 87 | ## Basic Git Commands 88 | 89 | #### 1. `git init` 90 | 91 | Initializes a new Git repository in the current directory. 92 | 93 | ```bash 94 | git init 95 | ``` 96 | 97 | #### 2. `git add` 98 | 99 | Adds files to the staging area, preparing them for a commit. 100 | 101 | ```bash 102 | git add filename.txt # Add a specific file 103 | git add . # Add all files in the current directory 104 | ``` 105 | 106 | #### 3. `git commit` 107 | 108 | Creates a new commit with the changes in the staging area. 109 | 110 | ```bash 111 | git commit -m "Your commit message here" 112 | ``` 113 | 114 | #### 4. `git status` 115 | 116 | Shows the current state of your working directory and staging area. 117 | 118 | ```bash 119 | git status 120 | ``` 121 | 122 | #### 5. `git log` 123 | 124 | Displays a log of all commits in the current branch. 125 | 126 | ```bash 127 | git log 128 | ``` 129 | 130 | ### Working with Branches 131 | 132 | #### 1. Creating a branch 133 | 134 | ```bash 135 | git branch new-feature 136 | ``` 137 | 138 | #### 2. Switching branches 139 | 140 | ```bash 141 | 142 | git checkout new-feature 143 | # Or, to create and switch in one command: 144 | git checkout -b another-feature 145 | ``` 146 | 147 | #### 3. Merging branches 148 | 149 | ```bash 150 | 151 | git checkout main 152 | git merge new-feature 153 | ``` 154 | 155 | ## Introduction to GitHub 156 | 157 | ### 1. What is GitHub? 158 | 159 | GitHub is a web-based platform that uses Git for version control. It provides additional collaboration features such as bug tracking, feature requests, task management, and wikis for every project. 160 | 161 | ### 2. Why use GitHub? 162 | 163 | - **Remote storage:** Acts as a backup for your local repository. 164 | - **Collaboration:** Makes it easy to share your code and collaborate with others. 165 | - **Open Source:** Facilitates contribution to open-source projects. 166 | - **Project Management:** Includes features like issue tracking and project boards. 167 | - **Continuous Integration/Continuous Deployment (CI/CD):** Integrates with various tools for automated testing and deployment. 168 | 169 | ### 3. GitHub Basics 170 | 171 | #### 1. Creating a GitHub account 172 | 173 | - Go to github.com 174 | - Click "Sign up" 175 | - Follow the prompts to create your account 176 | 177 | #### 2. Creating a repository on GitHub 178 | 179 | - Click the "+" icon in the top-right corner 180 | - Select "New repository" 181 | - Fill in the repository name and other details 182 | - Click "Create repository" 183 | 184 | #### 3. Cloning a repository 185 | 186 | To create a local copy of a GitHub repository: 187 | 188 | ```bash 189 | git clone https://github.com/username/repository-name.git 190 | ``` 191 | 192 | ### Collaborating with GitHub 193 | 194 | #### 1. Forking a repository 195 | 196 | Forking creates a personal copy of someone else's project. To fork a repository, click the "Fork" button on the GitHub page of the repository you want to fork. 197 | 198 | #### 2. Creating a Pull Request 199 | 200 | - Make changes in your forked repository 201 | - Go to the original repository 202 | - Click "New pull request" 203 | - Select "compare across forks" 204 | - Select your fork and branch 205 | - Click "Create pull request" 206 | 207 | ## Best Practices and Tips 208 | 209 | - **Commit often:** Make small, frequent commits rather than large, infrequent ones. 210 | - **Write clear commit messages:** Describe what changes were made and why. 211 | - **Use branches:** Create a new branch for each feature or bug fix. 212 | - **Review changes before committing:** Use git diff to review changes. 213 | - **Keep your repository clean:** Use .gitignore to exclude unnecessary files. 214 | - **Pull before you push:** Always pull the latest changes before pushing your own. 215 | - **Don't alter published history:** Avoid rewriting history that has been shared with others. 216 | -------------------------------------------------------------------------------- /12-modules/app.py: -------------------------------------------------------------------------------- 1 | # # import calculations 2 | # # import math 3 | # # import calculations, math 4 | # # from math import sqrt 5 | 6 | # # import calculations as rehan 7 | 8 | # from calculations import very_long_function_name_hard as usman 9 | 10 | # # rehan.very_long_function_name_hard() 11 | # usman() 12 | 13 | # print("This is main file") 14 | 15 | 16 | # # print (calculations.multiply) 17 | # # print (calculations.add) 18 | 19 | # # result = math.sqrt(9) 20 | # # print(result) 21 | 22 | # # ciel = "hi" 23 | 24 | 25 | from calculations import add 26 | 27 | first_number = int(input("Enter first number ")) 28 | second_number = int(input("Enter second number ")) 29 | 30 | print(add(first_number,second_number)) 31 | -------------------------------------------------------------------------------- /12-modules/calculations.py: -------------------------------------------------------------------------------- 1 | """Module for Arithematic Calculations""" 2 | 3 | 4 | def multiply(a: int, b: int) -> int: 5 | return a*b 6 | 7 | 8 | def add(a: int, b: int) -> int: 9 | return a+b 10 | 11 | 12 | def divide(a: int, b: int) -> float: 13 | return a/b 14 | 15 | 16 | def subtract(a: int, b: int) -> int: 17 | return a-b 18 | 19 | def very_long_function_name_hard (): 20 | pass 21 | 22 | 23 | if __name__ == "__main__": 24 | 25 | first_number = int(input("Enter first number ")) 26 | second_number = int(input("Enter second number ")) 27 | 28 | print(add(first_number,second_number)) 29 | 30 | print(subtract(10,9)) 31 | print(__name__) 32 | print("Hello i am in calculations.py file") -------------------------------------------------------------------------------- /12-modules/readme.md: -------------------------------------------------------------------------------- 1 | # Modules in Python 2 | 3 | ## Understanding Growing Code and the Need for Modules 4 | 5 | **1. Code Growth and User Expectations:** 6 | As users' needs evolve, so must the code. Any widely-used program will continually change to meet new demands. Unresponsive code becomes obsolete. 7 | 8 | **2. Growing Code = Growing Complexity:** 9 | Larger codebases are harder to maintain. Bugs are easier to spot in smaller, simpler programs, so as code grows, it requires better organization. 10 | 11 | **3. Why Split Code into Parts?** 12 | For large projects with many developers, it's impractical for everyone to work on the same file. Dividing the code into manageable parts is essential to avoid confusion and errors. 13 | 14 | 15 | **4. Modules: The Solution** 16 | To manage complexity, Python uses **modules**. A module is a file containing Python code that can be reused. It allows you to divide your program into smaller, manageable pieces. 17 | 18 | 19 | 20 | 1. **What is a Module?** 21 | A module is a Python file that contains functions, classes, or variables. You can import it into other Python files to reuse its code. 22 | 23 | 2. **Why Use Modules?** 24 | Modules help organize code, make it reusable, and improve maintainability by separating functionality. 25 | 26 | 3. **How to Create and Use Modules:** 27 | - Save your Python code in a `.py` file. For example, `math_utils.py`. 28 | - Use `import` to include this module in another file. 29 | 30 | ## Example 31 | 32 | **Creating a Module:** 33 | ```python 34 | # math_utils.py 35 | def add(a, b): 36 | return a + b 37 | 38 | def subtract(a, b): 39 | return a - b 40 | ``` 41 | 42 | **Using the Module in Another File:** 43 | ```python 44 | # main.py 45 | import math_utils 46 | 47 | result = math_utils.add(5, 3) 48 | print(result) # Output: 8 49 | ``` 50 | ## Usage Examples 51 | 52 | 53 | 1. **`import math`** 54 | In this case, you import the entire `math` module and can use its functions with the `math.` prefix. 55 | 56 | ```python 57 | import math 58 | 59 | # Example usage 60 | result = math.sqrt(16) # Using the sqrt function from math module 61 | print(result) # Output: 4.0 62 | ``` 63 | 64 | 2. **`import module1, module2, module3`** 65 | Here you import multiple custom modules (`module1`, `module2`, `module3`). Assuming these modules exist, you would use them similarly with the module name prefix. 66 | 67 | ```python 68 | import module1, module2, module3 69 | 70 | # Example usage (assuming the modules have relevant functions) 71 | module1.function1() 72 | module2.function2() 73 | module3.function3() 74 | ``` 75 | 76 | 3. **`from math import sin`** 77 | You are importing only the `sin` function from the `math` module, and can use it directly without the `math.` prefix. 78 | 79 | ```python 80 | from math import sin 81 | 82 | # Example usage 83 | result = sin(math.pi / 2) # Using sin directly 84 | print(result) # Output: 1.0 85 | ``` 86 | 87 | 4. **`from math import sin, pi`** 88 | You are importing both `sin` and `pi` from the `math` module and can use them directly without the `math.` prefix. 89 | 90 | ```python 91 | from math import sin, pi 92 | 93 | # Example usage 94 | result = sin(pi / 2) 95 | print(result) # Output: 1.0 96 | ``` 97 | 98 | 5. **`from math import *`** 99 | This imports everything from the `math` module, so you can access all functions and constants directly. 100 | 101 | ```python 102 | from math import * 103 | 104 | # Example usage 105 | result1 = sqrt(16) # Using sqrt directly 106 | result2 = cos(pi) # Using cos and pi directly 107 | print(result1, result2) # Output: 4.0 -1.0 108 | ``` 109 | 110 | 6. **`import math as rehan`** 111 | Here you are importing the `math` module with an alias `rehan`, and can use it with the alias instead of `math`. 112 | 113 | ```python 114 | import math as rehan 115 | 116 | # Example usage 117 | result = rehan.sqrt(16) # Using the sqrt function with the alias 118 | print(result) # Output: 4.0 119 | ``` 120 | 121 | 7. **`from math import sin as sine_funct`** 122 | You are importing the `sin` function and giving it an alias `sine_funct`, so you can use the alias instead of `sin`. 123 | 124 | ```python 125 | from math import sin as sine_funct 126 | 127 | # Example usage 128 | result = sine_funct(math.pi / 2) # Using the sin function as sine_funct 129 | print(result) # Output: 1.0 130 | ``` 131 | 132 | ## `if__name__ == "__main__":` Construct 133 | 134 | The `if __name__ == "__main__":` construct is a common Python idiom used to control whether certain parts of the code are executed when the script is run directly, as opposed to when it is imported as a module in another script. 135 | 136 | ### Explanation of `if __name__ == "__main__":` 137 | 138 | 1. **`__name__` Variable**: 139 | - In Python, the `__name__` variable is automatically set by the interpreter. When a Python file is executed, `__name__` is set to `"__main__"`. 140 | - If the file is imported as a module in another file, `__name__` will be set to the name of the file (the module name), without the `.py` extension. 141 | 142 | 2. **`if __name__ == "__main__":` Check**: 143 | - This check ensures that a block of code is only executed when the file is run directly (i.e., not when it is imported). 144 | - Code inside this block won't run if the file is being used as an imported module. 145 | 146 | ### Example: 147 | 148 | ```python 149 | # sample.py 150 | 151 | def greet(): 152 | print("Hello from the function!") 153 | 154 | if __name__ == "__main__": 155 | print("This code is running directly") 156 | greet() 157 | ``` 158 | 159 | ### When You Run the Script Directly: 160 | ```bash 161 | $ python sample.py 162 | ``` 163 | Output: 164 | ``` 165 | This code is running directly 166 | Hello from the function! 167 | ``` 168 | - Since the file is run directly, the `if __name__ == "__main__":` block executes. 169 | 170 | ### When the Script is Imported: 171 | ```python 172 | # another_script.py 173 | 174 | import sample 175 | 176 | sample.greet() 177 | ``` 178 | Output: 179 | ``` 180 | Hello from the function! 181 | ``` 182 | - Here, the message `"This code is running directly"` does not appear because the `if __name__ == "__main__":` block was bypassed when importing the module. 183 | 184 | ### When to Use It: 185 | 1. **For Testing**: If you want to include some test code or example usage within a script but avoid executing it when the script is imported elsewhere, use this construct. 186 | 187 | 2. **Script-Module Duality**: Sometimes, a script is designed to both provide a function (when imported) and work as a standalone program (when executed). The idiom helps separate these behaviors. 188 | 189 | ### When It Is Not Needed: 190 | 1. **Pure Library/Module**: If your script is only intended to be a module for import (and will never be executed directly), you don't need the `if __name__ == "__main__":` block. 191 | 192 | 2. **Top-Level Scripts**: If your script is always going to be executed directly and never imported, the block may not be necessary, although it's still a good practice to use it for flexibility. 193 | -------------------------------------------------------------------------------- /13-Final_Projects/project.md: -------------------------------------------------------------------------------- 1 | ## Final Project: Inventory Management System (IMS) 2 | 3 | ### Project Title: 4 | 5 | **Inventory Management System (IMS)** 6 | 7 | ### Objective: 8 | 9 | Build a console-based system that manages inventory for a small business. The system should allow admins to create, update, view, and delete products in the inventory while keeping track of stock levels and handling multiple users with role-based permissions. 10 | 11 | ### Requirements & Functionalities: 12 | 13 | 1. **User Authentication and Role Management** 14 | 15 | - Support different roles like “Admin” and “User.” 16 | - Admins can add, edit, and delete products, whereas Users can only view inventory details. 17 | - Implement a basic login system with username and password validation. 18 | 19 | 2. **Product Management (OOP Concepts)** 20 | 21 | - Create a `Product` class with attributes like `product_id`, `name`, `category`, `price`, and `stock_quantity`. 22 | - Create methods for adding, editing, and deleting products. 23 | - Store product information using lists or dictionaries. 24 | 25 | 3. **Inventory Operations** 26 | 27 | - Track stock levels: when stock reaches a low threshold, prompt a restocking message. 28 | - Implement methods for viewing all products, searching by product name or category, and filtering by stock levels. 29 | - Allow stock adjustments for existing products (e.g., restock or reduce inventory based on sales). 30 | 31 | 4. **Error Handling** 32 | - Ensure proper error handling for invalid inputs, such as incorrect login details or attempts to update non-existent products. 33 | - Use exceptions to handle potential issues, ensuring smooth flow. 34 | 35 | ### Learning Outcomes 36 | 37 | - **OOP principles**: Implement classes, encapsulation, and methods for CRUD operations. 38 | - **Data Structures**: Use dictionaries and lists for data storage and manipulation. 39 | - **Logic Building**: Develop functions for role-based access, transaction handling, and inventory management. 40 | - **Error Handling**: Practice exception handling for robust code. 41 | 42 | This project provides hands-on practice with the concepts they’ve learned while focusing on logic-based development without the need for external libraries or UI. 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cloud-native-modern-python-batch63 --------------------------------------------------------------------------------