Python-Handbook To Print
Python-Handbook To Print
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.
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.
• 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} ).
name = "Alice"
age = 25 print(type(10)) # Output: <class 'int'>
height = 5.6 print(type("Hello")) # Output: <class 'str'>
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:
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
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 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!"
• 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
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 = "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:
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
3. Keyword Arguments
print(factorial(5)) # Output: 120
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
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:
Parameters:
a (int): The first number.
b (int): The second number. 1. Lists and List Methods
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]
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
Dictionary Comprehensions:
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.
def speak(self):
5. Polymorphism: One Name, Many Forms
print("Generic animal sound")
• 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.
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.
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)
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).
@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.
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
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__ (/)
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.
filter may also be preferable when you already have a named function that within a list comprehension.
Introduction 3. Reading Files: You can read lines from a file and process them within a loop.
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.
• ‘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.
such as working with directories and files. The shutil module offers higher-level
file operations. import shutil
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:
• 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) ).
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
Uninstalling a package:
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())
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)
Multithreading
These techniques allow your programs to perform multiple tasks concurrently,
improving performance.
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()