0% found this document useful (0 votes)
11 views41 pages

Python-Handbook To Print

This document provides a comprehensive introduction to Python programming, including installation instructions, basic syntax, data types, and control flow structures. It covers essential concepts such as variables, functions, loops, and string manipulation, along with practical examples. Additionally, it highlights the importance of indentation and whitespace in Python code.

Uploaded by

xsg6969
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
11 views41 pages

Python-Handbook To Print

This document provides a comprehensive introduction to Python programming, including installation instructions, basic syntax, data types, and control flow structures. It covers essential concepts such as variables, functions, loops, and string manipulation, along with practical examples. Additionally, it highlights the importance of indentation and whitespace in Python code.

Uploaded by

xsg6969
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 41

2.

Download the latest version for your operating system (Windows, macOS,
or Linux).

1. Install Python:
Introduction to Python and
1. Run the installer and ensure you check the box to Add Python to PATH
Programming (important for running Python from the command line).

1. Verify Installation:
What is Programming?
1. Open a terminal or command prompt and type:
Programming is the process of giving instructions to a computer to perform
specific tasks. It involves writing code in a programming language that the python --version
computer can understand and execute.

1. This should display the installed Python version (e.g., Python 3.13.5 ).
Why Python?
1. Python is a high-level, interpreted programming language known for its Choosing an IDE
simplicity and readability.
1. What is an IDE?
2. It is widely used in:
2. An Integrated Development Environment (IDE) is a software application
3. Web Development (Django, Flask)
that provides tools for writing, testing, and debugging code.
4. Data Science and Machine Learning (Pandas, NumPy, TensorFlow)
3. Popular Python IDEs:
5. Automation and Scripting
4. VS Code: Lightweight, customizable, and supports extensions for Python.
6. Game Development (Pygame)
(We will use this one as our primary IDE)
7. Python has a large community and extensive libraries, making it
5. PyCharm: Powerful IDE with advanced features for professional
beginner-friendly.
developers.
6. Jupyter Notebook: Great for data science and interactive coding.
Setting Up Python and IDEs 7. IDLE: Comes pre-installed with Python; good for beginners.

Installing Python Writing Your First Python Program


1. Download Python:
The "Hello, World!" Program
1. Visit the official Python website: python.org.
1. Open a folder in your VS code and type the following code in a new file
named hello.py :
print("Hello, World!") if 5 > 2:
print("Five is greater than two!")
# Spaces before print are called indentation
1. Make sure to save the file with a .py extension (e.g., hello.py ).
2. Run the program:
1. Whitespace:
1. Use the run button at the top of your IDE or alternatively type this in your
1. Python is sensitive to whitespace. Ensure consistent indentation to avoid
VS Code integrated terminal:
errors. Ideally, use 4 spaces for indentation.

python hello.py 1. Statements:

1. Each line of code is a statement. You can write multiple statements on one
1. Output:
line using a semicolon ( ; ), but this is not recommended.

Hello, World! 1. Comments:

1. Use # for single-line comments.

Key Takeaways: 2. Use ''' or """ for multi-line comments.


3. Example:
1. print() is a built-in function used to display output.
2. Python code is executed line by line.
# This is a single-line comment
'''
Understanding Python Syntax and Basics This is a
multi-line comment
'''
Python Syntax Rules
1. Indentation:
Notes from Instructor
1. Python uses indentation (spaces or tabs) to define blocks of code.
2. Example: 1. Python is a versatile and beginner-friendly programming language.
2. Setting up Python and choosing the right IDE is the first step to writing
code.
3. Python syntax is simple but requires attention to indentation and
whitespace.
4. Start with small programs like "Hello, World!" to get comfortable with the
basics.
Data Types in Python
Python supports several built-in data types:

Python Fundamentals • Integers ( int ): Whole numbers (e.g., 10 , -5 ).


• Floats ( float ): Decimal numbers (e.g., 3.14 , -0.001 ).
• Strings ( str ): Text data enclosed in quotes (e.g., "Hello" , 'Python' ).
Variables and Data Types in Python • Booleans ( bool ): Represents True or False .
• Lists: Ordered, mutable collections (e.g., [1, 2, 3] ).
What are Variables? • Tuples: Ordered, immutable collections (e.g., (1, 2, 3) ).

• Variables are used to store data that can be used and manipulated in a • Sets: Unordered collections of unique elements (e.g., {1, 2, 3} ).
program. • Dictionaries: Key-value pairs (e.g., {"name": "Alice", "age": 25} ).

• A variable is created when you assign a value to it using the = operator.


Checking Data Types
• Example:
• Use the type() function to check the data type of a variable.

name = "Alice"
age = 25 print(type(10)) # Output: <class 'int'>
height = 5.6 print(type("Hello")) # Output: <class 'str'>

Variable Naming Rules Typecasting in Python


• Variable names can contain letters, numbers, and underscores.
• Variable names must start with a letter or underscore. What is Typecasting?
• Variable names are case-sensitive.
• Typecasting is the process of converting one data type to another.
• Avoid using Python keywords as variable names (e.g., print , if , else ).
• Python provides built-in functions for typecasting:
• int() : Converts to integer.
Best Practices
• float() : Converts to float.
• Use descriptive names that reflect the purpose of the variable. • str() : Converts to string.
• Use lowercase letters for variable names. • bool() : Converts to boolean.
• Separate words using underscores for readability (e.g., first_name ,
total_amount ).
Examples: Comments, Escape Sequences & Print Statement
# Convert string to integer
num_str = "10" Comments
num_int = int(num_str)
• Comments are used to explain code and are ignored by the Python
print(num_int) # Output: 10
interpreter.

# Convert integer to string


• Single-line comments start with # .
num = 25
num_str = str(num) • Multi-line comments are enclosed in ''' or """ .
print(num_str) # Output: "25"

# This is a single-line comment


# Convert float to integer
'''
pi = 3.14
This is a
pi_int = int(pi)
multi-line comment
print(pi_int) # Output: 3
'''

Taking User Input in Python Escape Sequences


• Escape sequences are used to include special characters in strings.
Using the input() Function
• Common escape sequences:
• The input() function allows you to take user input from the keyboard.
• \n : Newline
• By default, input() returns a string. You can convert it to other data types as • \t : Tab
needed.
• \\ : Backslash
• Example: • \" : Double quote
• \' : Single quote
name = input("Enter your name: ")
age = int(input("Enter your age: ")) • Example:
print(f"Hello {name}, you are {age} years old.")
print("Hello\nWorld!")
print("This is a tab\tcharacter.")

Print Statement
• The print() function is used to display output.
• You can use sep and end parameters to customize the output. 4. Assignment Operators:

1. = , += , -= , *= , /= , %= , **= , //= .
print("Hello", "World", sep=", ", end="!\n")
2. Example:

Operators in Python x = 10
x += 5 # Equivalent to x = x + 5
print(x) # Output: 15
Types of Operators
1. Arithmetic Operators: 5. Membership Operators:

1. + (Addition), - (Subtraction), * (Multiplication), / (Division), % 1. in , not in .

(Modulus), ** (Exponentiation), // (Floor Division).


2. Example:

2. Example:
fruits = ["apple", "banana", "cherry"]
print("banana" in fruits) # Output: True
print(10 + 5) # Output: 15
print(10 ** 2) # Output: 100
6. Identity Operators:

2. Comparison Operators:
1. is , is not .

1. == (Equal), != (Not Equal), > (Greater Than), < (Less Than), >=
2. Example:
(Greater Than or Equal), <= (Less Than or Equal).

2. Example: x = 10
y = 10
print(x is y) # Output: True
print(10 > 5) # Output: True
print(10 == 5) # Output: False

3. Logical Operators: Summary


1. and , or , not . • Variables store data, and Python supports multiple data types.
• Typecasting allows you to convert between data types.
2. Example:
• Use input() to take user input and print() to display output.

print(True and False) # Output: False • Comments and escape sequences help make your code more readable.
print(True or False) # Output: True • Python provides a variety of operators for performing operations on data.
print(not True) # Output: False
Match Case Statements in Python (Python 3.10+)

What is Match-Case?
Control Flow and Loops
• Match-case is a new feature introduced in Python 3.10 for pattern matching.
• It simplifies complex conditional logic.
If-Else Conditional Statements
Syntax:
What are Conditional Statements? match value:
• Conditional statements allow you to execute code based on certain conditions. case pattern1:
# Code to execute if value matches pattern1
• Python uses if , elif , and else for decision-making.
case pattern2:
# Code to execute if value matches pattern2
Syntax: case _:
# Default case (if no patterns match)
if condition1:
# Code to execute if condition1 is True
elif condition2: Example:
# Code to execute if condition2 is True
else: status = 404
# Code to execute if all conditions are False
match status:
case 200:
Example: print("Success!")
case 404:
age = 18 print("Not Found")
case _:
if age < 18: print("Unknown Status")
print("You are a minor.")
elif age == 18:
print("You just became an adult!")
else:
For Loops in Python
print("You are an adult.")
What are For Loops?
• For loops are used to iterate over a sequence (e.g., list, string, range).
• They execute a block of code repeatedly for each item in the sequence.
Syntax: Example:

for item in sequence: count = 0


# Code to execute for each item
while count < 5:
print(count)
Example: count += 1

fruits = ["apple", "banana", "cherry"]


Infinite Loops:
for fruit in fruits:
print(fruit) • Be careful to avoid infinite loops by ensuring the condition eventually
becomes False .

• Example of an infinite loop:


Using range() :

• The range() function generates a sequence of numbers. while True:


print("This will run forever!")
• Example:

for i in range(5):
Break, Continue, and Pass Statements
print(i) # Output: 0, 1, 2, 3, 4

Break
While Loops in Python • The break statement is used to exit a loop prematurely.

• Example:
What are While Loops?
• While loops execute a block of code as long as a condition is True . for i in range(10):
if i == 5:
• They are useful when the number of iterations is not known in advance.
break
print(i) # Output: 0, 1, 2, 3, 4
Syntax:

while condition:
Continue
# Code to execute while condition is True
• The continue statement skips the rest of the code in the current iteration
and moves to the next iteration.
• Example:

for i in range(5):
if i == 2:
Strings in Python
continue
print(i) # Output: 0, 1, 3, 4 Introduction
Strings are one of the most fundamental data types in Python. A string is a
Pass sequence of characters enclosed within either single quotes ( ' ), double quotes
( " ), or triple quotes ( ''' or “““).
• The pass statement is a placeholder that does nothing. It is used when syntax
requires a statement but no action is needed.
Creating Strings
• Example:
You can create strings in Python using different types of quotes:

for i in range(5):
# Single-quoted string
if i == 3:
a = 'Hello, Python!'
pass # Do nothing
print(i) # Output: 0, 1, 2, 3, 4
# Double-quoted string
b = "Hello, World!"

Summary # Triple-quoted string (useful for multi-line strings)


c = '''This is
• Use if , elif , and else for decision-making. a multi-line
• Use match-case for pattern matching (Python 3.10+). string.'''

• Use for loops to iterate over sequences and while loops for repeated
execution based on a condition.
String Indexing
• Control loop execution with break , continue , and pass .
Each character in a string has an index:

text = "Python"
print(text[0]) # Output: P
print(text[1]) # Output: y
print(text[-1]) # Output: n (last character)
String Slicing
message = '''
You can extract parts of a string using slicing: Hello,
This is a multi-line string example.
Goodbye!
text = "Hello, Python!"
'''
print(text[0:5]) # Output: Hello
print(message)
print(text[:5]) # Output: Hello
print(text[7:]) # Output: Python!
print(text[::2]) # Output: Hlo Pto!
Summary
• Strings are sequences of characters.
String Methods
• Use single, double, or triple quotes to define strings.
Python provides several built-in methods to manipulate strings: • Indexing and slicing allow accessing parts of a string.
• String methods help modify and manipulate strings.
text = " hello world " • f-strings provide an efficient way to format strings.
print(text.upper()) # Output: " HELLO WORLD "
print(text.lower()) # Output: " hello world "
print(text.strip()) # Output: "hello world" String Slicing and Indexing
print(text.replace("world", "Python")) # Output: " hello Python "
print(text.split()) # Output: ['hello', 'world']
Introduction
In Python, strings are sequences of characters, and each character has an index.
String Formatting You can access individual characters using indexing and extract substrings using

Python offers multiple ways to format strings: slicing.

name = "John" String Indexing


age = 25
Each character in a string has a unique index, starting from 0 for the first character
and -1 for the last character.
# Using format()
print("My name is {} and I am {} years old.".format(name, age))
text = "Python"
# Using f-strings (Python 3.6+) print(text[0]) # Output: P
print(f"My name is {name} and I am {age} years old.") print(text[1]) # Output: y
print(text[-1]) # Output: n (last character)
print(text[-2]) # Output: o
Multiline Strings
Triple quotes allow you to create multi-line strings:
String Slicing • Using [::-1] reverses a string.

Slicing allows you to extract a portion of a string using the syntax


string[start:stop:step] . String Methods and Functions

text = "Hello, Python!"


Introduction
print(text[0:5]) # Output: Hello
print(text[:5]) # Output: Hello (same as text[0:5]) Python provides a variety of built-in string methods and functions to manipulate
print(text[7:]) # Output: Python! (from index 7 to end) and process strings efficiently.
print(text[::2]) # Output: Hlo Pto!
print(text[-6:-1]) # Output: ython (negative indexing)
Common String Methods

Step Parameter Changing Case

The step parameter defines the interval of slicing. text = "hello world"
print(text.upper()) # Output: "HELLO WORLD"
print(text.lower()) # Output: "hello world"
text = "Python Programming"
print(text.title()) # Output: "Hello World"
print(text[::2]) # Output: Pto rgamn
print(text.capitalize()) # Output: "Hello world"
print(text[::-1]) # Output: gnimmargorP nohtyP (reverses string)

Removing Whitespace
Practical Uses of Slicing
text = " hello world "
String slicing is useful in many scenarios: - Extracting substrings - Reversing strings print(text.strip()) # Output: "hello world"
- Removing characters - Manipulating text efficiently print(text.lstrip()) # Output: "hello world "
print(text.rstrip()) # Output: " hello world"
text = "Welcome to Python!"
print(text[:7]) # Output: Welcome
Finding and Replacing
print(text[-7:]) # Output: Python!
print(text[3:-3]) # Output: come to Pyt
text = "Python is fun"
print(text.find("is")) # Output: 7
print(text.replace("fun", "awesome")) # Output: "Python is awesome"
Summary
• Indexing allows accessing individual characters.
Splitting and Joining
• Positive indexing starts from 0, negative indexing starts from -1.
• Slicing helps extract portions of a string. text = "apple,banana,orange"
fruits = text.split(",")
• The step parameter defines the interval for selection.
print(fruits) # Output: ['apple', 'banana', 'orange']
• Case conversion, trimming, finding, replacing, splitting, and joining are
new_text = " - ".join(fruits) commonly used.
print(new_text) # Output: "apple - banana - orange"
• Functions like len() , ord() , and chr() are useful for working with string
properties.
Checking String Properties

text = "Python123" String Formatting and f-Strings


print(text.isalpha()) # Output: False
print(text.isdigit()) # Output: False
Introduction
print(text.isalnum()) # Output: True
print(text.isspace()) # Output: False String formatting is a powerful feature in Python that allows you to insert variables
and expressions into strings in a structured way. Python provides multiple ways to
format strings, including the older .format() method and the modern
Useful Built-in String Functions f-strings .

len() - Get Length of a String Using .format() Method

text = "Hello, Python!" The .format() method allows inserting values into placeholders {} :
print(len(text)) # Output: 14

name = "Alice"
age = 30
ord() and chr() - Character Encoding
print("My name is {} and I am {} years old.".format(name, age))
print(ord('A')) # Output: 65
print(chr(65)) # Output: 'A' You can also specify positional and keyword arguments:

format() and f-strings print("{1} is learning {0}".format("Python", "Alice")) # Output: Alice is le


print("{name} is {age} years old".format(name="Bob", age=25))
name = "Alice"
age = 30
print("My name is {} and I am {} years old.".format(name, age)) f-Strings (Formatted String Literals)
print(f"My name is {name} and I am {age} years old.")
Introduced in Python 3.6, f-strings are the most concise and readable way to
format strings:

Summary name = "Alice"


age = 30
• Python provides various string methods for modification and analysis.
print(f"My name is {name} and I am {age} years old.")
Using Expressions in f-Strings

You can perform calculations directly inside f-strings:

x = 10 Functions and Modules


y = 5
print(f"The sum of {x} and {y} is {x + y}")
1. Defining Functions in Python
Formatting Numbers
Functions help in reusability and modularity in Python.
pi = 3.14159265
print(f"Pi rounded to 2 decimal places: {pi:.2f}") Syntax:

def greet(name):
Padding and Alignment
return f"Hello, {name}!"

text = "Python"
print(greet("Alice")) # Output: Hello, Alice!
print(f"{text:>10}") # Right align
print(f"{text:<10}") # Left align
print(f"{text:^10}") # Center align
Key Points:
• Defined using def keyword.
Important Notes
• Function name should be meaningful.
• Escape Sequences: Use \n , \t , \' , \" , and \\ to handle special
• Use return to send a value back.
characters in strings.
• Raw Strings: Use r"string" to prevent escape sequence interpretation.
• String Encoding & Decoding: Use .encode() and .decode() to work
with different text encodings. 2. Function Arguments & Return Values
• String Immutability: Strings in Python are immutable, meaning they
Functions can take parameters and return values.
cannot be changed after creation.
• Performance Considerations: Using ''.join(list_of_strings) is more
efficient than concatenation in loops.
Types of Arguments:
1. Positional Arguments
Summary
def add(a, b):
• .format() allows inserting values into placeholders.
return a + b
• f-strings provide an intuitive and readable way to format strings.
• f-strings support expressions, calculations, and formatting options.
4. Recursion in Python
print(add(5, 3)) # Output: 8

A function calling itself to solve a problem.


2. Default Arguments
Example: Factorial using Recursion
def greet(name="Guest"):
return f"Hello, {name}!" def factorial(n):
if n == 1:
print(greet()) # Output: Hello, Guest! return 1
return n * factorial(n-1)

3. Keyword Arguments
print(factorial(5)) # Output: 120

def student(name, age):


print(f"Name: {name}, Age: {age}")
Important Notes:
student(age=20, name="Bob") • Must have a base case to avoid infinite recursion.
• Used in algorithms like Fibonacci, Tree Traversals.

3. Lambda Functions in Python 5. Modules and Pip - Using External Libraries


Lambda functions are anonymous, inline functions.
Importing Modules
Syntax: Python provides built-in and third-party modules.

square = lambda x: x * x
Example: Using the math module
print(square(4)) # Output: 16
import math
Example:
print(math.sqrt(16)) # Output: 4.0

numbers = [1, 2, 3, 4]
squared = list(map(lambda x: x**2, numbers)) Creating Your Own Module
print(squared) # Output: [1, 4, 9, 16]
Save this as mymodule.py :
Example:
def greet(name):
return f"Hello, {name}!"
x = 10 # Global variable

Import in another file: def my_func():


x = 5 # Local variable
import mymodule print(x) # Output: 5
print(mymodule.greet("Alice")) # Output: Hello, Alice!
my_func()
print(x) # Output: 10 (global x remains unchanged)

Installing External Libraries with pip

pip install requests Using the global Keyword

To modify a global variable inside a function, use the global keyword:


Example usage:
x = 10 # Global variable
import requests
def modify_global():
response = requests.get("https://api.github.com") global x
print(response.status_code) x = 5 # Modifies the global x

modify_global()
print(x) # Output: 5
6. Function Scope and Lifetime

In Python, variables have scope (where they can be accessed) and lifetime (how This allows functions to change global variables, but excessive use of global is
long they exist). Variables are created when a function is called and destroyed discouraged as it can make debugging harder.
when it returns. Understanding scope helps avoid unintended errors and improves
code organization.
7. Docstrings - Writing Function Documentation
Types of Scope in Python Docstrings are used to document functions, classes, and modules. In Python, they
1. Local Scope (inside a function) – Variables declared inside a function are are written in triple quotes. They are accessible using the __doc__ attribute. Here’s
accessible only within that function. an example:

2. Global Scope (accessible everywhere) – Variables declared outside any


function can be used throughout the program. def add(a, b):
"""Returns the sum of two numbers."""
return a + b
print(add.__doc__) # Output: Returns the sum of two numbers.

Here is even proper way to write docstrings:


Data Structures in Python
def add(a, b):
Python provides powerful built-in data structures to store and manipulate
"""
collections of data efficiently.
Returns the sum of two numbers.

Parameters:
a (int): The first number.
b (int): The second number. 1. Lists and List Methods

Returns: Lists are ordered, mutable (changeable) collections of items.


int: The sum of the two numbers.
""" Creating a List:
return a + b
numbers = [1, 2, 3, 4, 5]
mixed = [10, "hello", 3.14]

Summary
Common List Methods:
• Functions help in reusability and modularity.
• Functions can take arguments and return values. my_list = [1, 2, 3]
• Lambda functions are short, inline functions.
• Recursion is a technique where a function calls itself. my_list.append(4) # [1, 2, 3, 4]
my_list.insert(1, 99) # [1, 99, 2, 3, 4]
• Modules help in organizing code and using external libraries.
my_list.remove(2) # [1, 99, 3, 4]
• Scope and lifetime of variables decide their accessibility. my_list.pop() # Removes last element -> [1, 99, 3]
• Docstrings are used to document functions, classes, and modules. my_list.reverse() # [3, 99, 1]
my_list.sort() # [1, 3, 99]

List Comprehensions (Efficient List Creation)

squared = [x**2 for x in range(5)]


print(squared) # Output: [0, 1, 4, 9, 16]
2. Tuples and Operations on Tuples Why Use Tuples?
• Faster than lists (since they are immutable)
Tuples are ordered but immutable collections (cannot be changed after creation).
• Used as dictionary keys (since they are hashable)
• Safe from unintended modifications
Creating a Tuple:

my_tuple = (10, 20, 30)


3. Sets and Set Methods
single_element = (5,) # Tuple with one element (comma required)

Sets are unordered, unique collections (no duplicates).

Accessing Tuple Elements:


Creating a Set:
print(my_tuple[1]) # Output: 20
fruits = {"apple", "banana", "cherry"}

Tuple Unpacking:
Key Set Methods:
a, b, c = my_tuple
print(a, b, c) # Output: 10 20 30 my_set = {1, 2, 3, 4}

my_set.add(5) # {1, 2, 3, 4, 5}
my_set.remove(2) # {1, 3, 4, 5}
Common Tuple Methods:
my_set.discard(10) # No error if element not found
Method Description Example Output my_set.pop() # Removes random element

Returns the number of


(1, 2, 2,
count(x) times x appears in 2
Set Operations:
3).count(2)
the tuple
a = {1, 2, 3}
Returns the index of
(10, 20, b = {3, 4, 5}
index(x) the first occurrence of 1
30).index(20)
x
print(a.union(b)) # {1, 2, 3, 4, 5}
print(a.intersection(b)) # {3}
my_tuple = (1, 2, 2, 3, 4) print(a.difference(b)) # {1, 2}
print(my_tuple.count(2)) # Output: 2
Use Case: Sets are great for eliminating duplicate values.
print(my_tuple.index(3)) # Output: 3
4. Dictionaries and Dictionary Methods Data Structure Features Best For

Set Unordered, Unique Removing duplicates, set operations


Dictionaries store key-value pairs and allow fast lookups.
Dictionary Key-Value Pairs Fast lookups, structured data
Creating a Dictionary:

student = {"name": "Alice", "age": 21, "grade": "A"}

Accessing & Modifying Values:

print(student["name"]) # Output: Alice


student["age"] = 22 # Updating value
student["city"] = "New York" # Adding new key-value pair

Common Dictionary Methods:

print(student.keys()) # dict_keys(['name', 'age', 'grade', 'city'])


print(student.values()) # dict_values(['Alice', 22, 'A', 'New York'])
print(student.items()) # dict_items([('name', 'Alice'), ('age', 22), ...])

student.pop("age") # Removes "age" key


student.clear() # Empties dictionary

Dictionary Comprehensions:

squares = {x: x**2 for x in range(5)}


print(squares) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

5. When to Use Each Data Structure?

Data Structure Features Best For

List Ordered, Mutable Storing sequences, dynamic data

Tuple Ordered, Immutable Fixed collections, dictionary keys


The Four Pillars of OOP

OOP is built on four fundamental principles:

Object-Oriented Programming (OOP) in 1. Abstraction: Think of driving a car. You use the steering wheel, pedals, and
gearshift, but you don’t need to know the complex engineering under the
Python hood. Abstraction means hiding complex details and showing only the
essential information to the user.
We’ll now explore how to organize and structure your Python code using objects, 2. Encapsulation: This is like putting all the car’s engine parts inside a protective
making it more manageable, reusable, and easier to understand. casing. Encapsulation bundles data (attributes) and the methods that operate
on that data within a class. This protects the data from being accidentally
changed or misused from outside the object. It controls access.
1. What is OOP Anyway?
3. Inheritance: Imagine creating a “SportsCar” class. Instead of starting from
Imagine you’re building with LEGOs. Instead of just having a pile of individual scratch, you can build it upon an existing “Car” class. The “SportsCar” inherits
bricks (like in procedural programming), OOP lets you create pre-assembled units – all the features of a “Car” (like wheels and an engine) and adds its own special
like a car, a house, or a robot. These units have specific parts (data) and things they features (like a spoiler). This promotes code reuse and reduces redundancy.
can do (actions). 4. Polymorphism: “Poly” means many, and “morph” means forms. This means
objects of different classes can respond to the same “message” (method call)
That’s what OOP is all about. It’s a way of programming that focuses on creating
in their own specific way. For example, both a “Dog” and a “Cat” might have a
“objects.” An object is like a self-contained unit that bundles together:
make_sound() method. The dog will bark, and the cat will meow – same

• Data (Attributes): Information about the object. For a car, this might be its method name, different behavior.
color, model, and speed.
• Actions (Methods): Things the object can do. A car can accelerate, brake, and 2. Classes and Objects: The Blueprint and the Building
turn.
• Class: Think of a class as a blueprint or a template. It defines what an object
Why Bother with OOP?
will be like – what data it will hold and what actions it can perform. It doesn’t
OOP offers several advantages: create the object itself, just the instructions for creating it. It’s like an
architectural plan for a house.
• Organization: Your code becomes more structured and easier to navigate.
Large projects become much more manageable. • Object (Instance): An object is a specific instance created from the class
• Reusability: You can use the same object “blueprints” (classes) multiple times, blueprint. If “Car” is the class, then your red Honda Civic is an object (an
saving you from writing the same code over and over. instance) of the “Car” class. Each object has its own unique set of data. It’s like
the actual house built from the architectural plan.
• Easier Debugging: When something goes wrong, it’s often easier to pinpoint
the problem within a specific, self-contained object. Let’s see this in Python:
• Real-World Modeling: OOP allows you to represent real-world things and
their relationships in a natural way. class Dog: # We define a class called "Dog"
species = "Canis familiaris" # A class attribute (shared by all Dogs)
3. The Constructor: Setting Things Up ( __init__ )
def __init__(self, name, breed): # The constructor (explained later)
self.name = name # An instance attribute to store the dog's name
The __init__ method is special. It’s called the constructor. It’s automatically run
self.breed = breed # An instance attribute to store the dog's breed
whenever you create a new object from a class.
def bark(self): # A method (an action the dog can do)
What’s it for? The constructor’s job is to initialize the object’s attributes – to give
print(f"{self.name} says Woof!")
them their starting values. It sets up the initial state of the object.

# Now, let's create some Dog objects:


my_dog = Dog("Buddy", "Golden Retriever") # Creating an object called my_dog class Dog:
another_dog = Dog("Lucy", "Labrador") # Creating another object def __init__(self, name, breed): # The constructor
self.name = name # Setting the name attribute
# We can access their attributes: self.breed = breed # Setting the breed attribute
print(my_dog.name) # Output: Buddy
print(another_dog.breed) # Output: Labrador # When we do this:
my_dog = Dog("Fido", "Poodle") # The __init__ method is automatically called
# And make them perform actions:
my_dog.bark() # Output: Buddy says Woof! # It's like we're saying:
print(Dog.species) # Output: Canis familiaris # 1. Create a new Dog object.
# 2. Run the __init__ method on this new object:
# - Set my_dog.name to "Fido"
• self Explained: Inside a class, self is like saying “this particular object.” It’s # - Set my_dog.breed to "Poodle"
a way for the object to refer to itself. It’s always the first parameter in a
method definition, but Python handles it automatically when you call the
You can also set default values for parameters in the constructor, making them
method. You don’t type self when calling the method; Python inserts it for
optional when creating an object:
you.

• Class vs. Instance Attributes: class Dog:


def __init__(self, name="Unknown", breed="Mixed"):
• Class Attributes: These are shared by all objects of the class. Like self.name = name
species in our Dog class. All dogs belong to the same species. They are self.breed = breed
defined outside of any method, directly within the class.
• Instance Attributes: These are specific to each individual object. name dog1 = Dog() # name will be "Unknown", breed will be "Mixed"
dog2 = Dog("Rex") # name will be "Rex", breed will be "Mixed"
and breed are instance attributes. Each dog has its own name and breed.
dog3 = Dog("Bella", "Labrador") # name will be "Bella", breed will be "Labrad
They are usually defined within the __init__ method.
4. Inheritance: Building Upon Existing Classes # Calling Parent Constructor with super()
class Bird(Animal):
Inheritance is like a family tree. A child class (or subclass) inherits traits (attributes def __init__(self, name, wingspan):
and methods) from its parent class (or superclass). This allows you to create new super().__init__(name) # Call Animal's __init__ to set the name
classes that are specialized versions of existing classes, without rewriting all the self.wingspan = wingspan # Add a Bird-specific attribute
code.
my_bird = Bird("Tweety", 10)
class Animal: # Parent class (superclass) print(my_bird.name) # Output: Tweety (set by Animal's constructor)
def __init__(self, name): print(my_bird.wingspan) # Output: 10 (set by Bird's constructor)
self.name = name

def speak(self):
5. Polymorphism: One Name, Many Forms
print("Generic animal sound")

Polymorphism, as we saw with the speak() method in the inheritance example,


class Dog(Animal): # Dog inherits from Animal (Dog is a subclass of Animal)
means that objects of different classes can respond to the same method call in
def speak(self): # We *override* the speak method (more on this later)
their own specific way. This allows you to write code that can work with objects of
print("Woof!")
different types without needing to know their exact class.

class Cat(Animal): # Cat also inherits from Animal


def speak(self):
6. Method Overriding: Customizing Inherited
print("Meow!")
Behavior
# Create objects:
my_dog = Dog("Rover") Method overriding is how polymorphism is achieved in inheritance. When a child
my_cat = Cat("Fluffy") class defines a method with the same name as a method in its parent class, the
child’s version overrides the parent’s version for objects of the child class. This allows
# They both have a 'name' attribute (inherited from Animal): specialized behavior in subclasses. The parent class’s method is still available (using
print(my_dog.name) # Output: Rover super() ), but when you call the method on a child class object, the child’s version
print(my_cat.name) # Output: Fluffy is executed.

# They both have a 'speak' method, but it behaves differently:


my_dog.speak() # Output: Woof! 7. Operator Overloading: Making Operators Work
my_cat.speak() # Output: Meow! with Your Objects

• super() : Inside a child class, super() lets you call methods from the parent Python lets you define how standard operators (like + , - , == ) behave when

class. This is useful when you want to extend the parent’s behavior instead of used with objects of your own classes. This is done using special methods called
completely replacing it. It’s especially important when initializing the parent “magic methods” (or “dunder methods” because they have double underscores
class’s part of a child object. before and after the name).
my_object.attribute ), you use methods to get and set its value. This might seem
class Point:
like extra work, but it provides significant advantages.
def __init__(self, x, y):
self.x = x
Why use them?
self.y = y
• Validation: You can add checks within the setter to make sure the attribute is
def __add__(self, other): # Overloading the + operator set to a valid value. For example, you could prevent an age from being
# 'other' refers to the object on the *right* side of the + negative.
return Point(self.x + other.x, self.y + other.y)
• Read-Only Attributes: You can create a getter without a setter, making the
attribute effectively read-only from outside the class. This protects the
def __str__(self): # String representation (for print() and str())
attribute from being changed accidentally.
return f"({self.x}, {self.y})"
• Side Effects: You can perform other actions when an attribute is accessed or
def __eq__(self, other): # Overloading == operator modified. For instance, you could update a display or log a change whenever a
return self.x == other.x and self.y == other.y value is set.
• Maintainability and Flexibility: If you decide to change how an attribute is
p1 = Point(1, 2) stored internally (maybe you switch from storing degrees Celsius to
p2 = Point(3, 4) Fahrenheit), you only need to update the getter and setter methods. You don’t
need to change every other part of your code that uses the attribute. This
p3 = p1 + p2 # This now works! It calls p1.__add__(p2)
makes your code much easier to maintain and modify in the future.
print(p3) # Output: (4, 6) (This uses the __str__ method)

print(p1 == p2) # Output: False (This uses the __eq__ method) class Person:
def __init__(self, name, age):
self.name = name
Other useful magic methods: (You don’t need to memorize them all, but be aware self._age = age # Convention: _age indicates it's intended to be "pr
they exist!)
def get_age(self): # Getter for age
• __sub__ ( - ), __mul__ ( * ), __truediv__ ( / ), __eq__ ( == ), __ne__
return self._age
( != ), __lt__ ( < ), __gt__ ( > ), __len__ ( len() ), __getitem__ ,
__setitem__ , __delitem__ (for list/dictionary-like behavior – allowing you def set_age(self, new_age): # Setter for age
to use [] with your objects). if new_age >= 0 and new_age <= 150: # Validation
self._age = new_age
else:
8. Getters and Setters: Controlling Access to print("Invalid age!")
Attributes
person = Person("Alice", 30)
Getters and setters are methods that you create to control how attributes of your print(person.get_age()) # Output: 30
class are accessed and modified. They are a key part of the principle of
encapsulation. Instead of directly accessing an attribute (like person.set_age(35)
print(person.get_age()) # Output: 35
It’s important to understand that Python does not have truly private attributes in
person.set_age(-5) # Output: Invalid age! the same way that languages like Java or C++ do. There’s no keyword that
print(person.get_age()) # Output: 35 (age wasn't changed) completely prevents access to an attribute from outside the class.

Instead, Python uses a convention: An attribute name starting with a single


The Pythonic Way: @property Decorator
underscore ( _ ) signals to other programmers that this attribute is intended for
Python offers a more elegant and concise way to define getters and setters using internal use within the class. It’s a strong suggestion: “Don’t access this directly
the @property decorator. This is the preferred way to implement them in modern from outside the class; use the provided getters and setters instead.” It’s like a
Python. “Please Do Not Touch” sign.

class Person: class MyClass:


def __init__(self, name, age): def __init__(self):
self.name = name self._internal_value = 0 # Convention: _ means "private"
self._age = age # Convention: _age for "private" attributes
def get_value(self):
@property # This makes 'age' a property (the getter) return self._internal_value
def age(self):
return self._age obj = MyClass()
# print(obj._internal_value) # This *works*, but it's against convention
@age.setter # This defines the setter for the 'age' property print(obj.get_value()) # This is the preferred way
def age(self, new_age):
if new_age >= 0 and new_age <= 150:
While you can still access obj._internal_value directly, doing so is considered
self._age = new_age
bad practice and can lead to problems if the internal implementation of the class
else:
changes. Always respect the underscore convention! It’s about good coding style
print("Invalid age!")
and collaboration.

person = Person("Bob", 40)


print(person.age) # Output: 40 (Looks like direct attribute access, but calls the getter)
person.age = 45 # (Calls the setter – looks like attribute assignment)
print(person.age)
person.age = -22 #Output: Invalid age!

With @property , accessing and setting the age attribute looks like you’re
working directly with a regular attribute, but you’re actually using the getter and
setter methods behind the scenes. This combines the convenience of direct access
with the control and protection of encapsulation.

Private Variables (and the _ convention):


@my_decorator
def say_hello():
print("Hello!")
Python: Advanced Concepts
say_hello()
This section covers several advanced concepts in Python, including decorators,
getters and setters, static and class methods, magic methods, exception handling,
Output:
map/filter/reduce, the walrus operator, and *args/**kwargs.
Something is happening before the function is called.
Hello!
Decorators in Python Something is happening after the function is called.

Introduction Here, @my_decorator is syntactic sugar for say_hello =

Decorators in Python are a powerful and expressive feature that allows you to my_decorator(say_hello) . It modifies the behavior of say_hello() by wrapping

modify or enhance functions and methods in a clean and readable way. They it inside wrapper() . The wrapper function adds behavior before and after the
provide a way to wrap additional functionality around an existing function without original function call.
permanently modifying it. This is often referred to as metaprogramming, where one
part of the program tries to modify another part of the program at compile time.

Decorators use Python’s higher-order function capability, meaning functions can Using Decorators with Arguments
accept other functions as arguments and return new functions.
Decorators themselves can also accept arguments. This requires another level of
nesting: an outer function that takes the decorator’s arguments and returns the
actual decorator function.
Understanding Decorators
def repeat(n):
A decorator is simply a callable (usually a function) that takes another function as
def decorator(func):
an argument and returns a replacement function. The replacement function def wrapper(a):
typically extends or alters the behavior of the original function. for _ in range(n):
func(a)
Basic Example of a Decorator return wrapper
return decorator
def my_decorator(func):
def wrapper():
@repeat(3)
print("Something is happening before the function is called.")
def greet(name):
func()
print(f"Hello, {name}!")
print("Something is happening after the function is called.")
return wrapper
Output:
greet("world")
HELLO!!!

Output:
Here, greet is first decorated by exclaim , and then the result of that is
Hello, world! decorated by uppercase . It’s equivalent to greet = uppercase(exclaim(greet)) .
Hello, world!
Hello, world!

In this example, repeat(3) returns the decorator function. The @ syntax then Recap
applies that returned decorator to greet . The argument in the wrapper function Decorators are a key feature in Python that enable code reusability and cleaner
ensures that the decorator can be used with functions that take any number of function modifications. They are commonly used for:
positional and keyword arguments.
• Logging: Recording when a function is called and its arguments.
• Timing: Measuring how long a function takes to execute.
• Authentication and Authorization: Checking if a user has permission to access
Chaining Multiple Decorators
a function.
You can apply multiple decorators to a single function. Decorators are applied from • Caching: Storing the results of a function call so that subsequent calls with the
bottom to top (or, equivalently, from the innermost to the outermost). same arguments can be returned quickly.
• Rate Limiting: Controlling how often a function can be called.
def uppercase(func): • Input Validation: Checking if the arguments to a function meet certain criteria.
def wrapper():
• Instrumentation: Adding monitoring and profiling to functions.
return func().upper()
return wrapper Frameworks like Flask and Django use decorators extensively for routing,
authentication, and defining middleware.
def exclaim(func):
def wrapper():
return func() + "!!!"
return wrapper
Getters and Setters in Python
@uppercase
@exclaim Introduction
def greet():
return "hello" In object-oriented programming, getters and setters are methods used to control
access to an object’s attributes (also known as properties or instance variables).
print(greet()) They provide a way to encapsulate the internal representation of an object,
allowing you to validate data, enforce constraints, and perform other operations
when an attribute is accessed or modified. While Python doesn’t have private
variables in the same way as languages like Java, the convention is to use a leading Using @property (Pythonic Approach)
underscore ( _ ) to indicate that an attribute is intended for internal use.
Python provides a more elegant and concise way to implement getters and setters
Using getters and setters helps: using the @property decorator. This allows you to access and modify attributes
using the usual dot notation (e.g., p.name ) while still having the benefits of getter
• Encapsulate data and enforce validation: You can check if the new value and setter methods.
meets certain criteria before assigning it.
• Control access to “private” attributes: By convention, attributes starting with class Person:
an underscore are considered private, and external code should use getters/ def __init__(self, name):
setters instead of direct access. self._name = name
• Make the code more maintainable: Changes to the internal representation of
an object don’t necessarily require changes to code that uses the object. @property
def name(self): # Getter
• Add additional logic: Logic can be added when getting or setting attributes.
return self._name

@name.setter
Using Getters and Setters def name(self, new_name): # Setter
self._name = new_name
Traditional Approach (Using Methods)
p = Person("Alice")
A basic approach is to use explicit getter and setter methods: print(p.name) # Alice (calls the getter)

class Person: p.name = "Bob" # Calls the setter


def __init__(self, name): print(p.name) # Bob
self._name = name # Convention: underscore (_) denotes a private attribute.

def get_name(self):
return self._name Benefits of @property :

def set_name(self, new_name): • Attribute-like access: You can use obj.name instead of obj.get_name() and
self._name = new_name obj.set_name() , making the code cleaner and more readable.

p = Person("Alice") • Consistent interface: The external interface of your class remains consistent
print(p.get_name()) # Alice even if you later decide to add validation or other logic to the getter or setter.
p.set_name("Bob")
• Read-only properties: You can create read-only properties by simply omitting
print(p.get_name()) # Bob
the @property.setter method (see the next section).

• @property.deleter : deletes a property. Here is an example:


def area(self): # Read-only computed property
class Person:
return 3.1416 * self._radius * self._radius
def __init__(self, name):
self._name = name c = Circle(5)
print(c.radius) # 5
@property print(c.area) # 78.54
def name(self): # Getter
return self._name # c.radius = 10 # Raises AttributeError: can't set attribute
# c.area = 20 # Raises AttributeError: can't set attribute
@name.setter
def name(self, new_name): # Setter
self._name = new_name

@name.deleter Recap
def name(self):
• Getters and Setters provide controlled access to an object’s attributes,
del self._name
promoting encapsulation and data validation.
p = Person("Alice") • The @property decorator offers a cleaner and more Pythonic way to
print(p.name) # Alice implement getters and setters, allowing attribute-like access.
del p.name • You can create read-only properties by defining only a getter (using
print(p.name) # AttributeError: 'Person' object has no attribute '_name'
@property without a corresponding @<attribute>.setter ).

• Using @property , you can dynamically compute values (like the area in the
Circle example) while maintaining an attribute-like syntax.

Read-Only Properties
If you want an attribute to be read-only, define only the @property decorator (the
getter) and omit the @name.setter method. Attempting to set the attribute will Static and Class Methods in Python
then raise an AttributeError .
Introduction
class Circle:
In Python, methods within a class can be of three main types:
def __init__(self, radius):
self._radius = radius • Instance Methods: These are the most common type of method. They operate
on instances of the class (objects) and have access to the instance’s data
@property
through the self parameter.
def radius(self):
return self._radius • Class Methods: These methods are bound to the class itself, not to any
particular instance. They have access to class-level attributes and can be used
@property to modify the class state. They receive the class itself (conventionally named
cls ) as the first argument.
• Static Methods: These methods are associated with the class, but they don’t @classmethod
have access to either the instance ( self ) or the class ( cls ). They are def set_species(cls, new_species):
cls.species = new_species # Modifies class attribute
essentially regular functions that are logically grouped within a class for
organizational purposes.
@classmethod
def get_species(cls):
return cls.species
Instance Methods (Default Behavior)
print(Animal.get_species()) # Mammal
Instance methods are the default type of method in Python classes. They require Animal.set_species("Reptile")
an instance of the class to be called, and they automatically receive the instance as print(Animal.get_species()) # Reptile
the first argument ( self ).
# You can also call class methods on instances, but it's less common:
a = Animal()
class Dog:
print(a.get_species()) # Reptile
def __init__(self, name):
self.name = name # Instance attribute
Example: Alternative Constructor
def speak(self):
return f"{self.name} says Woof!" class Person:
def __init__(self, name, age):
dog = Dog("Buddy") self.name = name
print(dog.speak()) # Buddy says Woof! self.age = age

@classmethod
def from_string(cls, data):
name, age = data.split("-")
Class Methods ( @classmethod )
return cls(name, int(age)) # Creates a new Person instance
A class method is marked with the @classmethod decorator. It takes the class itself
p = Person.from_string("Alice-30")
( cls ) as its first parameter, rather than the instance ( self ). Class methods are
print(p.name, p.age) # Alice 30
often used for:

• Modifying class attributes: They can change the state of the class, which In this example, from_string acts as a factory method, providing an alternative
affects all instances of the class. way to create Person objects from a string.
• Factory methods: They can be used as alternative constructors to create
instances of the class in different ways.

class Animal:
species = "Mammal" # Class attribute
Static Methods ( @staticmethod ) Can Access
Method Requires Requires Can Modify
Instance
Static methods are marked with the @staticmethod decorator. They are similar to Type self ? cls ? Class Attributes?
Attributes?
regular functions, except they are defined within the scope of a class.
Class
• They don’t take self or cls as parameters. ❌ No ✅ Yes ❌ No (directly) ✅ Yes
Method
• They are useful when a method is logically related to a class but doesn’t need
Static
to access or modify the instance or class state. ❌ No ❌ No ❌ No ❌ No
Method
• Often used for utility functions that are related to the class

class MathUtils:
@staticmethod
Recap
def add(a, b):
return a + b • Instance methods are the most common type and operate on individual
objects ( self ).
print(MathUtils.add(3, 5)) # 8
• Class methods operate on the class itself ( cls ) and are often used for factory
methods or modifying class-level attributes.
#Can also be called on an instance
m = MathUtils() • Static methods are utility functions within a class that don’t depend on the
print(m.add(4,5)) # 9 instance or class state. They’re like regular functions that are logically grouped
with a class.

When to Use Static Methods?

• When a method is logically related to a class but doesn’t require access to


instance-specific or class-specific data. Magic (Dunder) Methods in Python
• For utility functions that perform operations related to the class’s purpose
(e.g., mathematical calculations, string formatting, validation checks). Introduction
Magic methods, also called dunder (double underscore) methods, are special
methods in Python that have double underscores at the beginning and end of their
Key Differences Between Method Types names (e.g., __init__ , __str__ , __add__ ). These methods allow you to define
how your objects interact with built-in Python operators, functions, and language
Can Access
Method Requires Requires Can Modify constructs. They provide a way to implement operator overloading and customize
Instance
Type self ? cls ? Class Attributes? the behavior of your classes in a Pythonic way.
Attributes?
They are used to:
Instance ✅ Yes
✅ Yes ❌ No ✅ Yes
Method (indirectly) • Customize object creation and initialization ( __init__ , __new__ ).
• Enable operator overloading (e.g., + , - , * , == , < , > ).
• Provide string representations of objects ( __str__ , __repr__ ).
class Person:
• Control attribute access ( __getattr__ , __setattr__ , __delattr__ ). def __init__(self, name, age):
• Make objects callable ( __call__ ). self.name = name
self.age = age
• Implement container-like behavior ( __len__ , __getitem__ , __setitem__ ,
__delitem__ , __contains__ ).
def __str__(self):
• Support with context managers ( __enter__ , __exit__ ) return f"Person({self.name}, {self.age})" # User-friendly

def __repr__(self):
return f"Person(name='{self.name}', age={self.age})" # Unambiguous,
Common Magic Methods
p = Person("Alice", 30)
1. __init__ – Object Initialization print(str(p)) # Person(Alice, 30)
print(repr(p)) # Person(name='Alice', age=30)
The __init__ method is the constructor. It’s called automatically when a new
print(p) # Person(Alice, 30) # print() uses __str__ if available
instance of a class is created. It’s used to initialize the object’s attributes.

class Person: If __str__ is not defined, Python will use __repr__ as a fallback for str() and
def __init__(self, name, age): print() . It’s good practice to define at least __repr__ for every class you create.
self.name = name
self.age = age

3. __len__ – Define Behavior for len()


p = Person("Alice", 30)
print(p.name, p.age) # Alice 30 This method allows objects of your class to work with the built-in len() function.
It should return the “length” of the object (however you define that).

class Book:
2. __str__ and __repr__ – String Representation def __init__(self, title, pages):
self.title = title
• __str__ : This method should return a human-readable, informal string
self.pages = pages
representation of the object. It’s used by the str() function and by
print() . def __len__(self):
• __repr__ : This method should return an unambiguous, official string return self.pages
representation of the object. Ideally, this string should be a valid Python
expression that could be used to recreate the object. It’s used by the repr() b = Book("Python 101", 250)
print(len(b)) # 250
function and in the interactive interpreter when you just type the object’s
name and press Enter.
4. __add__ , __sub__ , __mul__ , etc. – Operator Overloading • __ge__ (>=)

These methods allow you to define how your objects behave with standard • __truediv__ (/)

arithmetic and comparison operators. • __floordiv__ (//)


• __mod__ (%)
class Vector:
• __pow__ (**)
def __init__(self, x, y):
self.x = x
self.y = y
Recap
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y) Magic (dunder) methods are a powerful feature of Python that allows you to:

def __sub__(self, other): • Customize how your objects interact with built-in operators and functions.
return Vector(self.x - other.x, self.y - other.y) • Make your code more intuitive and readable by using familiar Python syntax.
• Implement operator overloading, container-like behavior, and other advanced
def __mul__(self, scalar): features.
return Vector(self.x * scalar, self.y * scalar)
• Define string representation.

def __str__(self):
return f"Vector({self.x}, {self.y})"

v1 = Vector(2, 3)
Exception Handling and Custom Errors in Python
v2 = Vector(4, 5)
v3 = v1 + v2 # Calls __add__ Introduction
print(v3) # Vector(6, 8)
v4 = v3 - v1 Exceptions are events that occur during the execution of a program that disrupt
print(v4) # Vector(4, 5) the normal flow of instructions. Python provides a robust mechanism for handling
v5 = v1 * 5 exceptions using try-except blocks. This allows your program to gracefully
print(v5) # Vector(10, 15) recover from errors or unexpected situations, preventing crashes and providing
informative error messages. You can also define your own custom exceptions to
Other common operator overloading methods include: represent specific error conditions in your application.

• __eq__ (==)
• __ne__ (!=)
Basic Exception Handling
• __lt__ (<)
• __gt__ (>) The try-except block is the fundamental construct for handling exceptions:

• __le__ (<=) • The try block contains the code that might raise an exception.
• The except block contains the code that will be executed if a specific Using else and finally
exception occurs within the try block.
• else : The else block is optional and is executed only if no exception occurs
within the try block. It’s useful for code that should run only when the try
try:
block succeeds.
x = 10 / 0 # This will raise a ZeroDivisionError
• finally : The finally block is also optional and is always executed,
except ZeroDivisionError:
print("Cannot divide by zero!") regardless of whether an exception occurred or not. It’s typically used for
cleanup operations, such as closing files or releasing resources.

Output:
try:
file = open("test.txt", "r")
Cannot divide by zero!
content = file.read()
except FileNotFoundError:
print("File not found!")
else:
Handling Multiple Exceptions print("File read successfully.")
print(f"File contents:\n{content}")
You can handle multiple types of exceptions using multiple except blocks or by finally:
specifying a tuple of exception types in a single except block. file.close() # Ensures the file is closed no matter what

try:
num = int(input("Enter a number: "))
result = 10 / num
Raising Exceptions ( raise )
except ZeroDivisionError:
print("You can't divide by zero!") You can manually raise exceptions using the raise keyword. This is useful for
except ValueError:
signaling error conditions in your own code.
print("Invalid input! Please enter a number.")

def check_age(age):
# Alternative using a tuple:
if age < 18:
try:
raise ValueError("Age must be 18 or older!")
num = int(input("Enter a number: "))
return "Access granted."
result = 10 / num
except (ZeroDivisionError, ValueError) as e:
try:
print(f"An error occurred: {e}")
print(check_age(20)) # Access granted.
print(check_age(16)) # Raises ValueError
except ValueError as e:
print(f"Error: {e}")
• The else block executes only if no exception occurs in the try block.

Custom Exceptions • The finally block always executes, making it suitable for cleanup tasks.
• The raise keyword allows you to manually trigger exceptions.
Python allows you to define your own custom exception classes by creating a new
class that inherits (directly or indirectly) from the built-in Exception class (or one • Custom exceptions (subclasses of Exception ) provide a way to represent

of its subclasses). This makes your error handling more specific and informative. application-specific errors and improve error handling clarity.

class InvalidAgeError(Exception):
"""Custom exception for invalid age."""
def __init__(self, message="Age must be 18 or older!"): Map, Filter, and Reduce
self.message = message
super().__init__(self.message)
Introduction
def verify_age(age): map , filter , and reduce are higher-order functions in Python (and many other
if age < 18: programming languages) that operate on iterables (lists, tuples, etc.). They provide
raise InvalidAgeError() # Raise your custom exception a concise and functional way to perform common operations on sequences of data
return "Welcome!" without using explicit loops. While they were more central to Python’s functional
programming style in earlier versions, list comprehensions and generator
try:
expressions often provide a more readable alternative in modern Python.
print(verify_age(16))
except InvalidAgeError as e:
print(f"Error: {e}") Map
The map() function applies a given function to each item of an iterable and
By defining custom exceptions, you can: returns an iterator that yields the results.

• Create a hierarchy of exceptions that reflect the specific error conditions in Syntax: map(function, iterable, ...)
your application.
• function : The function to apply to each item.
• Provide more informative error messages tailored to your application’s needs.
• Make it easier for other parts of your code (or other developers) to handle • iterable : The iterable (e.g., list, tuple) whose items will be processed.

specific errors appropriately. • ... : map can take multiple iterables. The function must take the same
number of arguments

numbers = [1, 2, 3, 4, 5]
Conclusion
• try-except blocks are essential for handling errors and preventing program # Square each number using map
crashes. squared_numbers = map(lambda x: x**2, numbers)
print(list(squared_numbers)) # Output: [1, 4, 9, 16, 25]
• Multiple except blocks or a tuple of exception types can be used to handle
different kinds of errors.
#Example with multiple iterables Reduce
numbers1 = [1, 2, 3]
numbers2 = [4, 5, 6] The reduce() function applies a function of two arguments cumulatively to the
summed = map(lambda x, y: x + y, numbers1, numbers2) items of an iterable, from left to right, so as to reduce the iterable to a single value.
print(list(summed)) # Output: [5, 7, 9] reduce is not a built-in function; it must be imported from the functools
module.

# Equivalent list comprehension: Syntax: reduce(function, iterable[, initializer])


squared_numbers_lc = [x**2 for x in numbers]
print(squared_numbers_lc) # Output: [1, 4, 9, 16, 25] • function : A function that takes two arguments.
• iterable : The iterable to be reduced.
• initializer (optional): If provided, it’s placed before the items of the
Filter iterable in the calculation and serves as a default when the iterable is empty.
The filter() function constructs an iterator from elements of an iterable for
which a function returns True . In other words, it filters the iterable based on a from functools import reduce
condition.
numbers = [1, 2, 3, 4, 5]
Syntax: filter(function, iterable)
# Calculate the sum of all numbers using reduce
• function : A function that returns True or False for each item. If None is sum_of_numbers = reduce(lambda x, y: x + y, numbers)
passed, it defaults to checking if the element is True (truthy value). print(sum_of_numbers) # Output: 15
• iterable : The iterable to be filtered.
# Calculate the product of all numbers using reduce
product_of_numbers = reduce(lambda x, y: x * y, numbers)
numbers = [1, 2, 3, 4, 5, 6]
print(product_of_numbers) # Output: 120

# Get even numbers using filter


#reduce with initializer
even_numbers = filter(lambda x: x % 2 == 0, numbers)
empty_list_sum = reduce(lambda x,y: x+y, [], 0)
print(list(even_numbers)) # Output: [2, 4, 6]
print(empty_list_sum) # 0

# Equivalent list comprehension:


# Without the initializer:
even_numbers_lc = [x for x in numbers if x % 2 == 0]
# empty_list_sum = reduce(lambda x,y: x+y, []) # raises TypeError
print(even_numbers_lc) # Output: [2, 4, 6]

# Equivalent using a loop (for sum):


# Example with None as function
total = 0
values = [0, 1, [], "hello", "", None, True, False]
for x in numbers:
truthy_values = filter(None, values)
total += x
print(list(truthy_values)) # Output: [1, 'hello', True]
print(total) # 15
When to use map, filter, reduce vs. list comprehensions/generator expressions: while data != "quit":
print(f"You entered: {data}")
• Readability: List comprehensions and generator expressions are often more data = input("Enter a value (or 'quit' to exit): ")
readable and easier to understand, especially for simple operations.
# With walrus operator
• Performance: In many cases, list comprehensions/generator expressions can while (data := input("Enter a value (or 'quit' to exit): ")) != "quit":
be slightly faster than map and filter . print(f"You entered: {data}")

• Complex Operations: reduce can be useful for more complex aggregations


where In the “with walrus” example, the input is assigned to data and compared to
“quit” in a single expression.
• Complex Operations: reduce can be useful for more complex aggregations
where the logic is not easily expressed in a list comprehension. map and 2. List Comprehensions: You can avoid repeated calculations or function calls

filter may also be preferable when you already have a named function that within a list comprehension.

you want to apply.


numbers = [1, 2, 3, 4, 5]
• Functional Programming Style: If you’re working in a more functional
programming style, map , filter , and reduce can fit naturally into your # Without walrus operator: calculate x * 2 twice
code. results = [x * 2 for x in numbers if x * 2 > 5]

# With walrus operator: calculate x * 2 only once


Walrus Operator (:=) results = [y for x in numbers if (y := x * 2) > 5]

Introduction 3. Reading Files: You can read lines from a file and process them within a loop.

The walrus operator ( := ), introduced in Python 3.8, is an assignment expression


# Without Walrus
operator. It allows you to assign a value to a variable within an expression. This can
with open("my_file.txt", "r") as f:
make your code more concise and, in some cases, more efficient by avoiding line = f.readline()
repeated calculations or function calls. The name “walrus operator” comes from the while line:
operator’s resemblance to the eyes and tusks of a walrus. print(line.strip())
line = f.readline()
Use Cases
# With Walrus
1. Conditional Expressions: The most common use case is within if with open("my_file.txt", "r") as f:
statements, while loops, and list comprehensions, where you need to both while (line := f.readline()):
test a condition and use the value that was tested. print(line.strip())

# Without walrus operator


data = input("Enter a value (or 'quit' to exit): ")
Considerations my_function() # No output (empty tuple)
my_function("a", "b") # Output: a b
• Readability: While the walrus operator can make code more concise, it can
also make it harder to read if overused. Use it judiciously where it improves
In this example, *args collects all positional arguments passed to my_function
clarity.
into the args tuple.
• Scope: The variable assigned using := is scoped to the surrounding block
(e.g., the if statement, while loop, or list comprehension).
**kwargs (Keyword Arguments)
• Precedence: The walrus operator has lower precedence than most other
operators. Parentheses are often needed to ensure the expression is evaluated **kwargs collects any extra keyword arguments passed to a function into a
as intended. dictionary. Again, kwargs is the conventional name, but you could use any valid
variable name preceded by two asterisks (e.g., **data , **options ).

def my_function(**kwargs):
Args and Kwargs print(type(kwargs)) # <class 'dict'>
for key, value in kwargs.items():
Introduction print(f"{key}: {value}")

*args and **kwargs are special syntaxes in Python function definitions that my_function(name="Alice", age=30, city="New York")
allow you to pass a variable number of arguments to a function. They are used # Output:
when you don’t know in advance how many arguments a function might need to # name: Alice
accept. # age: 30
# city: New York
• *args : Allows you to pass a variable number of positional arguments.
• **kwargs : Allows you to pass a variable number of keyword arguments. my_function() # No output (empty dictionary)
my_function(a=1, b=2)
# Output:
*args (Positional Arguments) # a: 1
# b: 2
*args collects any extra positional arguments passed to a function into a tuple.
The name args is just a convention; you could use any valid variable name
preceded by a single asterisk (e.g., *values , *numbers ). In this example, **kwargs collects all keyword arguments into the kwargs
dictionary.

def my_function(*args):
print(type(args)) # <class 'tuple'> Combining *args and **kwargs
for arg in args:
print(arg) You can use both *args and **kwargs in the same function definition. The order
is important: *args must come before **kwargs . You can also include regular
my_function(1, 2, 3, "hello") # Output: 1 2 3 hello positional and keyword parameters.
def __init__(self, name):
def my_function(a, b, *args, c=10, **kwargs):
self.name = name
print(f"a: {a}")
print(f"b: {b}") class Dog(Animal):
print(f"args: {args}") def __init__(self, name, breed, *args, **kwargs):
print(f"c: {c}") super().__init__(name)
print(f"kwargs: {kwargs}") self.breed = breed
# Process any additional arguments or keyword arguments here
my_function(1, 2, 3, 4, 5, c=20, name="Bob", country="USA")
print(f"args: {args}")
# Output: print(f"kwargs: {kwargs}")
# a: 1
# b: 2 dog1 = Dog("Buddy", "Golden Retriever")
# args: (3, 4, 5)
dog2 = Dog("Lucy", "Labrador", 1,2,3, color="Black", age = 5)
# c: 20
# kwargs: {'name': 'Bob', 'country': 'USA'}

my_function(1,2)
# Output:
# a: 1
# b: 2
# args: ()
# c: 10
# kwargs: {}

Use Cases
• Flexible Function Design: *args and **kwargs make your functions more
flexible, allowing them to handle a varying number of inputs without needing
to define a specific number of parameters.
• Decorator Implementation: Decorators often use *args and **kwargs to
wrap functions that might have different signatures.
• Function Composition: You can use *args and **kwargs to pass arguments
through multiple layers of function calls.
• Inheritance: Subclasses can accept extra parameters to those defined by
parent classes.

# Example showing use in inheritance


class Animal:
content = file.read() # Read the entire file content
print(content)
file.close() # Close the file
Section 9: File Handling and OS Operations except FileNotFoundError:
print("File not found.")
This section introduces you to file handling in Python, which allows your programs
to interact with files on your computer. We’ll also explore basic operating system
(OS) interactions using Python’s built-in modules. # Reading line by line
try:
file = open("my_file.txt", "r")
File I/O in Python
for line in file: # Efficient for large files
File Input/Output (I/O) refers to reading data from and writing data to files. Python print(line.strip()) # Remove newline characters
provides built-in functions to make this process straightforward. Working with files file.close()
except FileNotFoundError:
generally involves these steps:
print("File not found.")
1. Opening a file: You need to open a file before you can read from it or write to
it. This creates a connection between your program and the file. Writing to a file:
2. Performing operations: You can then read data from the file or write data to
it. file = open("new_file.txt", "w") # Open in write mode (creates or overwrites
3. Closing the file: It’s crucial to close the file when you’re finished with it. This file.write("Hello, world!\n") # Write some text
releases the connection and ensures that any changes you’ve made are saved. file.write("This is a new line.\n")
file.close()

Read, Write, and Append Files


Appending to a file:
Python provides several modes for opening files:

• ‘r’ (Read mode): Opens the file for reading. This is the default mode. If the file file = open("my_file.txt", "a") # Open in append mode
doesn’t exist, you’ll get an error. file.write("This is appended text.\n")
file.close()
• ‘w’ (Write mode): Opens the file for writing. If the file exists, its contents will
be overwritten. If the file doesn’t exist, a new file will be created.
• ‘a’ (Append mode): Opens the file for appending. Data will be added to the Using with statement (recommended):
end of the file. If the file doesn’t exist, a new file will be created.
The with statement provides a cleaner way to work with files. It automatically
Here are some examples: closes the file, even if errors occur.

Reading from a file:


try:
with open("my_file.txt", "r") as file:
try: content = file.read()
file = open("my_file.txt", "r") # Open in read mode print(content)
except FileNotFoundError: # Check if a file or directory exists
print("File not found.") if os.path.exists("my_file.txt"):
print("File exists")
with open("output.txt", "w") as file:
file.write("Data written using 'with'.\n") # Join path components in a platform-independent way
path = os.path.join("folder", "subfolder", "file.txt")
print("Joined path:", path)
OS and Shutil Modules in Python
Python’s os module provides functions for interacting with the operating system, shutil module examples:

such as working with directories and files. The shutil module offers higher-level
file operations. import shutil

os module examples: # Copy a file


# shutil.copy("my_file.txt", "my_file_copy.txt")
import os
# Move a file or directory
# Get the current working directory # shutil.move("my_file.txt", "new_directory/")
current_dir = os.getcwd()
print("Current directory:", current_dir)
Creating Command Line Utilities
# Create a new directory
You can use Python to create simple command-line utilities. The argparse module
# os.mkdir("new_directory") # creates only one level of directory
makes it easier to handle command-line arguments.
# os.makedirs("path/to/new_directory") # creates nested directories

# Change the current directory import argparse


# os.chdir("new_directory")
parser = argparse.ArgumentParser(description="A simple command-line utility."
# List files and directories in a directory parser.add_argument("filename", help="The file to process.")
files = os.listdir(".") # "." represents current directory parser.add_argument("-n", "--number", type=int, default=1, help="Number of ti
print("Files in current directory:", files)
args = parser.parse_args()
# Remove a file or directory
# os.remove("my_file.txt") try:
# os.rmdir("new_directory") # removes empty directory with open(args.filename, "r") as file:
# shutil.rmtree("path/to/new_directory") # removes non-empty directory (use with caution) content = file.read()
for _ in range(args.number):
# Rename a file or directory print(content)
# os.rename("old_name.txt", "new_name.txt")
except FileNotFoundError:
print("File not found.")

To run this script from the command line:


Section 10: Working with External Libraries

This section introduces you to the world of external libraries in Python. These
python my_script.py my_file.txt -n 3
libraries extend Python’s capabilities and allow you to perform complex tasks more
easily. We’ll cover virtual environments, package management, working with APIs,
This will print the contents of my_file.txt three times. You can learn more about regular expressions, and asynchronous programming.
argparse in the Python documentation.

Virtual Environments & Package Management


As you start working on more Python projects, you’ll likely use different versions of
libraries. Virtual environments help isolate project dependencies, preventing
conflicts between different projects.

Virtual Environments:

A virtual environment is a self-contained directory that contains its own Python


interpreter and libraries. This means that libraries installed in one virtual
environment won’t interfere with libraries in another.

Creating a virtual environment (using venv - recommended):

python3 -m venv my_env # Creates a virtual environment named "my_env"

Activating the virtual environment:

• Windows: my_env\Scripts\activate
• macOS/Linux: source my_env/bin/activate

Once activated, you’ll see the virtual environment’s name in your terminal prompt
(e.g., (my_env) ).

Package Management (using pip ):

pip is Python’s package installer. It’s used to install, upgrade, and manage
external libraries.

Installing a package:
response = requests.get(url)
pip install requests # Installs the "requests" library
pip install numpy==1.20.0 # Installs a specific version if response.status_code == 200:
data = response.json() # Parse the JSON response
Listing installed packages: print(data["name"]) # Access data from the JSON
else:
print(f"Error: {response.status_code}")
pip list

# Making a POST request (for sending data to an API):


Upgrading a package: # data = {"key": "value"}
# response = requests.post(url, json=data) # Sends data as JSON
pip install --upgrade requests
# Other HTTP methods: put(), delete(), etc.

Uninstalling a package:

Regular Expressions in Python


pip uninstall requests
Regular expressions (regex) are powerful tools for pattern matching in strings.
Python’s re module provides support for regex.
Generating a requirements file:

A requirements.txt file lists all the packages your project depends on. This import re
makes it easy to recreate the environment on another machine.
text = "The quick brown fox jumps over the lazy dog."
pip freeze > requirements.txt # Creates the requirements file
pip install -r requirements.txt # Installs packages from the file # Search for a pattern
match = re.search("brown", text)
if match:
Deactivating the virtual environment: print("Match found!")
print("Start index:", match.start())
deactivate print("End index:", match.end())

# Find all occurrences of a pattern


Requests Module - Working with APIs matches = re.findall("the", text, re.IGNORECASE) # Case-insensitive search
print("Matches:", matches)
The requests library simplifies making HTTP requests. This is essential for
interacting with web APIs (Application Programming Interfaces). # Replace all occurrences of a pattern
new_text = re.sub("fox", "cat", text)
import requests print("New text:", new_text)

url = "https://api.github.com/users/octocat" # Example API endpoint # Compile a regex for efficiency (if used multiple times)
pattern = re.compile(r"\b\w+\b") # Matches whole words
words = pattern.findall(text)
print("Words:", words)

Lets understand the regex pattern re.compile(r"\b\w+\b") used in the above


code: | Part | Meaning | |——|———| | \b | Word boundary (ensures we match full
words, not parts of words) | | \w+ | One or more word characters (letters, digits,
underscores) | | \b | Word boundary (ensures we match entire words) |

Multithreading
These techniques allow your programs to perform multiple tasks concurrently,
improving performance.

Multithreading (using threading module):

Multithreading is suitable for I/O-bound tasks (e.g., waiting for network requests).

import threading
import time

def worker(num):
print(f"Thread {num}: Starting")
time.sleep(2) # Simulate some work
print(f"Thread {num}: Finishing")

threads = []
for i in range(3):
thread = threading.Thread(target=worker, args=(i,))
threads.append(thread)
thread.start()

for thread in threads:


thread.join() # Wait for all threads to finish

print("All threads completed.")

You might also like