0% found this document useful (0 votes)
5 views90 pages

Unit 2 Python

This document provides an overview of defining and using functions in Python, including their components, syntax, and how to call them. It explains the importance of functions for code reuse, modular programming, and various types of function arguments such as positional, keyword, and default arguments. Additionally, it covers the use of return values and the organization of code through modules and packages.

Uploaded by

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

Unit 2 Python

This document provides an overview of defining and using functions in Python, including their components, syntax, and how to call them. It explains the importance of functions for code reuse, modular programming, and various types of function arguments such as positional, keyword, and default arguments. Additionally, it covers the use of return values and the organization of code through modules and packages.

Uploaded by

siva
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

Unit 2

Functions and Files


2.1 Defining and Using Functions
2.1.1 What is a function in Python?

A function in Python is a named, reusable block of code that performs a specific


task. It is defined using the def keyword and helps in organizing code, improving
readability, and avoiding repetition (the "Don't Repeat Yourself" or DRY principle).
Function Components
A typical Python function has the following parts:
 def keyword: Marks the beginning of the function definition.

 Function name: A descriptive name that follows variable naming rules.


 Parentheses () : May contain parameters (placeholders for input data).

 Colon : : Signifies the end of the function header.

 Indented body: The block of code that runs when the function is called.
 return statement (optional): Used to send a value back to the caller. If
omitted, the function automatically returns None .

Example of a Python Function


Here is a simple example of a Python function that takes two numbers as input and
returns their sum:
python
# Function Definition
def add_numbers(a, b):
"""
This function takes two numbers and returns their sum.
"""
result = a + b
return result

# Function Call
num1 = 10
num2 = 5
sum_result = add_numbers(num1, num2)

# Output the result


print("The sum is:", sum_result)
Output:
The sum is: 15

Explanation of the Example


1. def add_numbers(a, b): : This line defines a function
named add_numbers that accepts two parameters, a and b .

2. """...""" : This is a docstring, which is a brief description of what the


function does.

3. result = a + b : The function's body, where it performs the task of adding


the two inputs.

4. return result : This sends the value stored in result back to where the
function was called.

5. sum_result = add_numbers(num1, num2) : This calls the function


with num1 and num2 as arguments and stores the returned value in
the sum_result variable.

2.1.2 Syntax of function definition using `def`


The syntax for defining a function in Python using the def keyword is
straightforward and relies on specific components, including indentation to define the
function's body.
Basic Syntax
python
def function_name(parameters):
"""Docstring (optional): description of the function."""
# Function body (indented code block)
# Perform actions
return value # Optional: return a value

Key Components
 def keyword: This signals the start of a function definition.
 function_name : A descriptive name for your function, following Python's
naming conventions (lowercase with underscores, e.g., calculate_area ).

 Parentheses () : Required after the function name. Optional parameters are


placed inside, separated by commas.
 Colon : : This terminates the function header line and indicates the start of
the function body.

 Indentation: The code block within the function (its "body") must be
consistently indented, typically by four spaces. This is how Python knows
which statements belong to the function.
 """Docstring""" (optional): A string literal right after the definition line that

documents the function's purpose. It is enclosed in triple quotes.


 return statement (optional): Used to send a value back to the code that
called the function. If omitted, the function automatically returns None .

Example
Here is a function that calculates the area of a rectangle, demonstrating the syntax
with parameters and a return value:
python
def calculate_area(length, width):
"""Calculates the area of a rectangle given length and width."""
area = length * width
return area

# Calling the function


rectangle_area = calculate_area(5, 10)
print(f"The area is: {rectangle_area}")
# Output: The area is: 50

2.1.3 Calling a function


To call a function in Python, you write the function's name followed
by parentheses () . If the function requires input, you include the
necessary arguments inside the parentheses.
Calling a Function with No Arguments
First, you must define the function using the def keyword. The code inside the
function body is executed only when the function is called.
python
# 1. Define the function
def greet():
print("Hello, World!")

# 2. Call the function


greet()
Output:
Hello, World!
When greet() is called, the program's control transfers to the function definition,
executes the print() statement, and then returns to the next line after the function
call.
Calling a Function with Arguments
Many functions take inputs, called arguments, to perform their task. You pass these
arguments inside the parentheses during the call.
python
# 1. Define the function with parameters
def add_numbers(num1, num2):
sum_result = num1 + num2
print(f"Sum: {sum_result}")

# 2. Call the function with arguments


add_numbers(5, 4)
Output:
Sum: 9
The values 5 and 4 (arguments) are assigned to the
parameters num1 and num2 respectively based on their position.
Using a Function's Return Value
If a function computes a value and uses the return statement, you can capture that
value in a variable or use it directly in an expression.
python
# 1. Define a function that returns a value
def find_square(num):
result = num * num
return result

# 2. Call the function and store the result


square = find_square(3)

# 3. Use the returned value


print(f"Square: {square}")
Output:
Square: 9
The value 9 returned by find_square(3) is assigned to the variable square .
Key Points
 Parentheses are essential: Forgetting the parentheses after the function
name will return a reference to the function object itself, not execute its code.

 Order matters (for positional arguments): When using positional


arguments, the order in the function call must match the order of parameters
in the function definition.

 Reusability: The primary benefit of functions is that they can be called


multiple times throughout your code, which avoids repetition and makes your
code more organized.

2.1.4 Return values with `return`


In Python, the return statement is used inside a function to send a value (or
values) back to the caller. This returned value can then be used for further
operations, assigned to a variable, or passed as an argument to another function.
How to Return a Single Value
Use the return keyword followed by the value or expression you want to return.
The function immediately stops executing after the return statement is run.
python
def multiply(number1, number2):
return number1 * number2 # The result of the expression is returned

# Call the function and store the returned value in a variable


result = multiply(6, 8)
print(result) # Output: 48

# You can also use the return value directly


print(multiply(2, 2) + 10) # Output: 14

How to Return Multiple Values


Python functions can return multiple values by listing them after
the return statement, separated by commas. Python automatically packs these
values into a single tuple.
The caller can then unpack these values into separate variables.
python
def get_user_info():
name = "John Doe"
email = "[Link]@[Link]"
# Return multiple values
return name, email # Python packs this into a tuple automatically

# Unpack the returned tuple into separate variables


user_name, user_email = get_user_info()

print(f"Name: {user_name}, Email: {user_email}")


# Output: Name: John Doe, Email: [Link]@[Link]

# The result is actually a tuple if assigned to a single variable


user_data = get_user_info()
print(user_data) # Output: ('John Doe', '[Link]@[Link]')

Returning Other Data Types


You can return any Python object, including lists, dictionaries, or even other
functions.
 Returning a list:

python
def get_even_numbers(numbers_list):
# Use a list comprehension to create and return a list of even
numbers
return [num for num in numbers_list if num % 2 == 0]

even_nums = get_even_numbers([1, 2, 3, 4, 5, 6])


print(even_nums) # Output: [2, 4, 6]
 Returning a dictionary:

python
def get_stats(sample_data):
# Return a dictionary with labeled values
return {
'mean': sum(sample_data) / len(sample_data),
'count': len(sample_data)
}

data_summary = get_stats([1, 2, 3, 4])


print(data_summary['mean']) # Output: 2.5
Implicit None Return
If a function does not have a return statement, or if return is used without a
value, the function will automatically return the special value None .
python
def greet(name):
print(f"Hello, {name}!")
# No explicit return statement

result = greet("World")
print(f"The return value is: {result}")
# Output:
# Hello, World!
# The return value is: None

2.1.5 Code reuse and modular programming


In Python, code reuse and modular programming are achieved primarily through
the use of functions, modules, and packages. This approach involves breaking
down large programs into smaller, independent, and interchangeable parts to
improve organization, maintainability, and efficiency.
Functions for Code Reuse
Functions are the most basic and essential form of code reuse in Python. A function
is a named block of code that performs a specific, single task.
 DRY Principle: Functions help you follow the "Do Not Repeat Yourself"
(DRY) principle by allowing you to write a piece of code once and execute it
multiple times by simply "calling" the function.

 Encapsulation: They wrap up the implementation details, so the main


program only needs to know the function's purpose and how to call it, not the
internal mechanics.

 Example:

python
def calculate_area(radius):
"""Calculates the area of a circle given its radius."""
pi = 3.14159
area = pi * radius ** 2
return area
# Reuse the function multiple times
area1 = calculate_area(5)
area2 = calculate_area(10)
This function calculate_area can be called from anywhere in your script,
with different inputs, without rewriting the area calculation logic each time.
Modules for Modular Programming
A module is simply a Python file ( .py ) containing related functions, classes, and
variables. Modular programming involves partitioning your code across different files
based on logical boundaries, much like organizing files into folders on a computer.
 Organization and Simplicity: Instead of one large, unwieldy script, you
break the program into smaller, focused files. Each module handles a specific
aspect of the functionality
(e.g., data_processing.py , helper_functions.py ).

 Importing: Code from one module can be accessed in another using


the import statement.

 Namespace Management: Modules create a separate namespace, which


helps avoid naming collisions between variables and functions in different
parts of the program.

 Collaboration and Debugging: This structure makes it easier for multiple


developers to work on different parts of the program simultaneously and
simplifies debugging, as errors are isolated to a specific module.

How to Use Modules


1. Create a module: Save functions in a file, for example, math_utils.py .

python
# math_utils.py
def add(a, b):
return a + b

def subtract(a, b):


return a - b
2. Import and use in another script: In a separate file (e.g., [Link] ), you
can import and use these functions.

python
# [Link]
import math_utils

result = math_utils.add(5, 3)
print(f"5 + 3 = {result}")
# Output: 5 + 3 = 8

from math_utils import subtract


# Can also import specific functions directly
result2 = subtract(10, 4)
print(f"10 - 4 = {result2}")
# Output: 10 - 4 = 6

Packages for Large-Scale Modularity


As projects grow even larger, modules can be organized into packages. A package
is a directory (folder) that contains multiple module files, often with
an __init__.py file (though not strictly required in Python 3.3+) to signify it as a
package. This hierarchical structure allows for robust, large-scale application
development.
By mastering these techniques, you can write clean, maintainable, and scalable
Python code.

2.2 Function Arguments


In Python, arguments are values provided to a function when it is called,
corresponding to the parameters defined in the function signature. Python supports
several types of function arguments:
 Positional Arguments: These are arguments assigned to parameters based
on their order in the function call. The number and order of arguments must
match the function definition.

python
def greet(fname, lname):
print(f"Hello, {fname} {lname}")

greet("Emil", "Refsnes") # Positional arguments


 Keyword Arguments: Arguments are passed by explicitly specifying the
parameter name using key=value syntax. The order does not matter in this
case, which improves code readability.

python
def greet(fname, lname):
print(f"Hello, {fname} {lname}")

greet(lname="Refsnes", fname="Emil") # Keyword arguments, order


doesn't matter
 Default Arguments: Parameters can have a predefined default value in the
function definition. If an argument is not provided during the function call, the
default value is used.

python
def greet(fname, lname="Smith"):
print(f"Hello, {fname} {lname}")

greet("Emil") # Uses default "Smith" for lname


greet("Emil", "Refsnes") # Overrides default value
 Variable-length Arguments ( *args and **kwargs ): These allow a function
to accept an arbitrary number of arguments.
o *args (non-keyword arguments) collects an arbitrary number of

positional arguments into a tuple inside the function.

python
def sum_numbers(*args):
return sum(args)

print(sum_numbers(1, 2, 3, 4)) # Output: 10


o **kwargs (keyword arguments) collects an arbitrary number of

keyword arguments into a dictionary inside the function.

python
def introduce(**kwargs):
for key, value in [Link]():
print(f"{key}: {value}")

introduce(name="Tobias", age=30, city="Bergen")

Key Rules for Combining Arguments


When combining different argument types in a function definition, they must follow a
specific order:
1. Regular parameters (positional or keyword)
2. *args

3. **kwargs

2.2.1 Positional arguments


In Python, positional arguments are a way of passing values to a function where
the arguments' values are matched to the function's parameters based solely on
their order (position). The first argument in the function call is assigned to the first
parameter in the function definition, the second argument to the second parameter,
and so on.
Key Characteristics
 Order Matters: The most important rule is that the order of the arguments in
the function call must exactly match the order of the parameters in the
function definition. If the order is changed, the function might execute without
error, but produce an unexpected or incorrect result.

 No Names Required: When using positional arguments, you do not use the
parameter names in the function call, only the values themselves.

 Required Arguments: All defined parameters for which a positional argument


is expected must receive a value, unless a default value has been specified in
the function definition.

Example
Here is a simple example demonstrating positional arguments:
python
def describe_pet(animal_type, pet_name):
"""Prints a description of a pet."""
print(f"I have a {animal_type} named {pet_name}.")

# Correct usage with positional arguments:


describe_pet("dog", "Buddy")

# Output: I have a dog named Buddy.

# Incorrect order (will produce unexpected results):


describe_pet("Buddy", "dog")

# Output: I have a Buddy named dog.


In the first call, "dog" is matched to animal_type and "Buddy" to pet_name . In
the second call, the order is reversed, leading to a functional but semantically
incorrect output.
Mixing with Keyword Arguments
Positional arguments can be used in combination with keyword arguments. A key
rule to remember is that all positional arguments must appear before any
keyword arguments in the function call.
python
def mixed_arguments(arg1, arg2, arg3):
print(f"arg1: {arg1}, arg2: {arg2}, arg3: {arg3}")

# Valid call with mixed arguments:


mixed_arguments(10, 20, arg3=30)
# Output: arg1: 10, arg2: 20, arg3: 30

# Invalid call (positional argument after keyword argument):


# mixed_arguments(10, arg2=20, 30) # This would raise a SyntaxError

Positional-Only Parameters (Python 3.8+)


Python 3.8 introduced a way to explicitly define function parameters that can only be
passed as positional arguments. This is done by placing a forward slash ( / ) in the
function definition's parameter list. All parameters before the / are positional-only.
python
def force_positional(name, /, greeting="Hello"):
return f"{greeting}, {name}!"

# Valid calls:
print(force_positional("Alice"))
# Output: Hello, Alice!

print(force_positional("Bob", "Hi"))
# Output: Hi, Bob!

# Invalid call (cannot use 'name' as a keyword):


# print(force_positional(name="Charlie"))
# This would raise a TypeError
For more in-depth information, you can refer to the official Python documentation or
explore resources on [GeeksforGeeks]([Link]
[Link]/python/keyword-and-positional-argument-in-python/).
2.2.2 Keyword arguments
In Python, keyword arguments are values passed into a function by explicitly
naming the corresponding parameter, using the syntax key=value . This approach
makes the order of the arguments in the function call irrelevant and enhances code
readability.
Key Characteristics
 Order does not matter: Unlike positional arguments, you can pass keyword
arguments in any order, as the value is tied to the parameter name, not its
position.

 Improved readability: Explicitly naming parameters makes it clear what each


value represents, which is especially useful for functions with many
parameters.

 Flexibility with default values: Keyword arguments work seamlessly with


parameters that have default values, allowing you to only specify the
arguments you need to change.

Examples
Basic Usage
In this example, the order of the arguments in the function call does not affect the
outcome:
python
def team(name, project):
print(f"{name} is working on an {project}")

# Using positional arguments (order matters)


team("FemCode", "Answers")
# Output: FemCode is working on an Answers

# Using keyword arguments (order does not matter)


team(project="Answers", name="FemCode")
# Output: FemCode is working on an Answers

Mixing Positional and Keyword Arguments


When using both types of arguments in a single function call, the positional
arguments must always come first.
python
def greet(first_name, last_name):
print(f"Hello, {first_name} {last_name}")
# Valid: Positional first, then keyword
greet("Anna", last_name="Brown")

# Invalid: Positional argument after a keyword argument


# greet(first_name="Anna", "Brown") # This would raise a SyntaxError

Variable-Length Keyword Arguments ( **kwargs )


Python also allows functions to accept an arbitrary or variable number of keyword
arguments using the special syntax **kwargs in the function definition. This collects
any extra keyword arguments into a dictionary.
python
def print_details(**details):
for key, value in [Link]():
print(f"{key}: {value}")

print_details(name="Alice", age=30, city="New York")


# Output:
# name: Alice
# age: 30
# city: New York

2.2.3 Default parameter values


In Python, you can define default parameter values in a function definition using
the assignment operator ( = ). This makes the corresponding argument optional
when the function is called; if no value is provided, the default value is used.
Syntax and Usage
Parameters with default values must be placed after any required parameters in the
function's definition.
python
def greet(name, message="Hello"):
print(f"{message}, {name}!")

# Calling the function


greet("Alice") # Uses the default message "Hello"
greet("Bob", "Good morning") # Overrides the default message with "Good
morning"

Key Characteristics
 Flexibility: Default parameters allow a single function to be called with a
varying number of arguments, making code more flexible and readable.

 Overriding: Providing an argument during the function call will always


override the default value.

 Order: Required parameters must always precede parameters with default


values to avoid a SyntaxError .

Common Pitfall: Mutable Default Arguments


A critical concept in Python is that default arguments are evaluated only once,
when the function is defined, not each time it is called. This behavior leads to
unexpected results when using mutable data types (such as lists or dictionaries) as
default values, as all function calls will share the same instance of the mutable
object.
Problematic Example
python
def add_item(item, item_list=[]):
item_list.append(item)
return item_list

print(add_item('apple'))
print(add_item('banana'))
# Expected output: ['apple'], ['banana']
# Actual output: ['apple'], ['apple', 'banana']

Recommended Solution (using None )


To avoid this issue, use None as the default value and create a new mutable object
inside the function body each time it is needed.
python
def add_item_safe(item, item_list=None):
if item_list is None:
item_list = []
item_list.append(item)
return item_list

print(add_item_safe('apple'))
print(add_item_safe('banana'))
# Output: ['apple']
# Output: ['banana']
2.2.4 Variable-length arguments: `*args` and `kwargs`
In Python, *args and **kwargs allow functions to accept a variable number of
arguments. *args handles non-keyworded (positional) arguments,
while **kwargs handles keyworded (named) arguments. The
names args and kwargs are by convention; the single ( * ) and double ( ** )
asterisks are the key syntax elements.
*args (Variable Positional Arguments)
The *args syntax collects all extra positional arguments into a tuple inside the
function. This is useful when you want to create a function that can operate on an
arbitrary number of inputs, such as a summation function.
Example:
python
def sum_all(*args):
"""Calculates the sum of all provided numbers."""
total = 0
for num in args:
total += num
return total

# Calling the function with a variable number of arguments


print(sum_all(1, 2, 3)) # Output: 6
print(sum_all(10, 20, 30, 40)) # Output: 100

**kwargs (Variable Keyword Arguments)


The **kwargs syntax collects all extra keyword arguments (arguments in the
form key=value ) into a dictionary inside the function. This is useful for functions
that might have optional, named parameters.
Example:
python
def print_info(**kwargs):
"""Prints the key-value pairs of provided keyword arguments."""
print("Received arguments as a dictionary:", kwargs)
for key, value in [Link]():
print(f"{key}: {value}")

# Calling the function with variable keyword arguments


print_info(name='Alice', age=30, city='New York')
# Output:
# Received arguments as a dictionary: {'name': 'Alice', 'age': 30, 'city':
'New York'}
# name: Alice
# age: 30
# city: New York

Combined Example ( *args and **kwargs )


You can use both *args and **kwargs in the same function definition. When
combining them, the correct order is: regular arguments, then *args ,
then **kwargs .
python
def combined_example(arg1, arg2, *args, **kwargs):
"""Demonstrates using regular, positional, and keyword arguments."""
print("Regular argument 1:", arg1)
print("Regular argument 2:", arg2)
print("Positional arguments (*args):", args)
print("Keyword arguments (**kwargs):", kwargs)

# Calling the function with a mix of arguments


combined_example(
1,
2,
3, 4, 5, # Captured by *args as a tuple (3, 4, 5)
name='Bob', age=42 # Captured by **kwargs as a dictionary
{'name': 'Bob', 'age': 42}
)

2.2.5 Argument unpacking


Argument unpacking in Python is the process of automatically extracting elements
from an iterable (like a list, tuple, or dictionary) and passing them as individual
arguments to a function using the * and ** operators.
How Unpacking Works
The operators used for unpacking differ based on the type of arguments being
passed:
 * (single asterisk): Used to unpack positional arguments from a sequence

(e.g., list, tuple, or generator). The elements are assigned to the function's
parameters in order.
 ** (double asterisk): Used to unpack keyword arguments from a dictionary.

The keys of the dictionary must match the names of the parameters in the
function definition.

Examples
Unpacking Positional Arguments ( * )
This is useful when you have a collection of values and want them to be treated as
separate inputs to a function.
python
def addition(a, b, c):
return a + b + c

numbers = [1, 5, 10]


result = addition(*numbers)
print(f"Sum: {result}")
# Output: Sum: 16
In this example, *numbers unpacks the list so that a becomes 1 , b becomes 5 ,
and c becomes 10 .
Unpacking Keyword Arguments ( ** )
This is useful for dynamically passing data stored as key-value pairs to a function.
python
def info(name, age, country):
print(f"Name: {name}, Age: {age}, Country: {country}")

data = {"name": "Alice", "age": 30, "country": "USA"}


info(**data)
# Output: Name: Alice, Age: 30, Country: USA
Here, **data unpacks the dictionary, matching the keys
( "name" , "age" , "country" ) to the corresponding function parameters.
Other Use Cases
Unpacking also has general uses outside of function calls:
 Parallel Assignment: A concise way to assign multiple values at once.

python
x, y, z = [1, 2, 3]
 Swapping Variables: Swapping values between two variables without a
temporary variable.

python
a = 10
b = 20
a, b = b, a
 Ignoring Values: Using an underscore ( _ ) with the asterisk ( *_ ) to ignore
certain values during unpacking.

python
first, *_, last = (1, 2, 3, 4, 5, 6)
# first is 1, last is 6, the middle values are ignored

2.2.6 Scope of variables in functions (local vs. global)


The scope of a variable defines the region of a program where that variable can be
accessed. Variables declared within functions have local scope, meaning they are
accessible only within that function, while variables declared outside of all functions
have global scope and are accessible throughout the entire program.
Local Variables
 Declaration: Declared inside a function or a specific block of code (e.g.,
within { } braces of a loop or conditional statement).

 Accessibility: Only accessible within the function or block where they are
declared. They are not recognized outside their scope.

 Lifetime: Created when the function/block is entered and destroyed when the
function/block finishes executing. This promotes efficient memory use.

 Naming: Different functions can use the same variable name without causing
conflicts because their scopes are isolated.

 Use Cases: Ideal for temporary storage or calculations relevant only within a
specific function, promoting encapsulation and preventing unintended side
effects in other parts of the code.

Global Variables
 Declaration: Declared outside of any function, typically at the top of the file.

 Accessibility: Accessible from anywhere in the program, including inside all


functions and blocks, after the point of declaration.

 Lifetime: Persist throughout the entire execution of the program.


 Naming: Must have unique names, as they can be accessed and modified by
any part of the program, increasing the risk of naming conflicts in large
projects.

 Use Cases: Useful for storing constants (e.g., the value of Pi) or data that
needs to be shared across many different parts of the program, such as a
user session ID.

Key Differences at a Glance


Aspect Local Variables Global Variables

Accessibility Limited to the function/block Accessible from anywhere in


where declared the program

Declaration Inside functions or code blocks Outside of any function,


usually at the top

Lifetime Created on function entry, Persists throughout the


destroyed on function exit program's entire execution

Name Same name can be used in Must have a unique name


Conflicts different functions program-wide

Memory Stored on the stack Stored in a data segment

2.3 Returning Values


In Python, the return statement is used to exit a function and pass a value or
multiple values back to the caller. This allows the result of a function to be stored in a
variable and used in other parts of the program.
Key Concepts
 return keyword: This statement immediately terminates the function's

execution and sends the specified value back to the caller. Any code after
a return statement is not executed.

 Assigning the result: The value returned by a function can be assigned to a


variable in the calling code.
 Implicit None return: If a function does not have a return statement, or has
a return statement without a value, it automatically returns the special
value None .

 Printing vs. Returning: The print() function simply displays output to the
console, while the return statement makes data available for further
computation within the program.

Examples
Returning a Single Value
To return a single value, use the return keyword followed by the value or
expression.
python
def multiply(a, b):
result = a * b
return result

# The returned value is stored in a variable


product = multiply(5, 4)
print(product)
# Output: 20
Alternatively, you can return an expression directly:
python
def multiply(a, b):
return a * b

product = multiply(5, 4)
print(product)
# Output: 20
You can also use the returned value directly in other expressions or function calls,
such as a print() statement:
python
print(multiply(5, 4))
# Output: 20
Returning Multiple Values
Python functions can return multiple values by separating them with commas. These
values are automatically packed into a tuple.
Functions can return multiple values as a tuple, which can then be unpacked into
separate variables.
python
def get_user_info():
name = "John Doe"
email = "[Link]@[Link]"
return name, email

user_name, user_email = get_user_info()


print(f"Name: {user_name}, Email: {user_email}")
# Output: Name: John Doe, Email: [Link]@[Link]

2.3.1 Understanding `return` statement


The return statement in Python has two primary functions within a
function: to immediately stop the function's execution and to send a value back
to the part of the code that called it.
How it Works
 Exits the function: When the Python interpreter encounters
a return statement, it terminates the current function's execution
immediately. Any code following the return statement in that function (often
called "dead code") will not run.
 Sends a value back: The return keyword is typically followed by a value,
variable, or expression. This "return value" is passed back to the caller.

Key Concepts and Examples


Returning a Single Value
The most common use is to return a single piece of data, which can be stored in a
variable or used in further operations.
python
def add_numbers(a, b):
result = a + b
return result # Returns the value of 'result'

# The returned value (8) is stored in the 'sum_value' variable


sum_value = add_numbers(5, 3)
print(sum_value)
# Output: 8
In this case, the function call add_numbers(5, 3) is replaced by the value 8 when
the code runs.
Returning Multiple Values
Python functions can return multiple values by listing them after
the return statement, separated by commas. These values are automatically
packed into a single tuple.
python
def get_user_info():
name = "Alice"
age = 30
return name, age # Automatically creates a tuple ('Alice', 30)

# Unpack the returned tuple into separate variables


user_name, user_age = get_user_info()

print(f"Name: {user_name}, Age: {user_age}")


# Output: Name: Alice, Age: 30
Implicit None Return
If a function does not have an explicit return statement, or if a return statement
is used without a value, the function will automatically return the special
value None .
python
def greet(name):
print(f"Hello, {name}!")
# No return statement, so None is returned implicitly

result = greet("World")
print(f"The function returned: {result}")
# Output:
# Hello, World!
# The function returned: None
Early Exit
The return statement can be used with conditional logic to exit a function early
based on certain conditions, which can improve efficiency and code clarity.
python
def validate_age(age):
if age < 0 or age > 120:
return False # Exit early if age is invalid
return True # If the code reaches this point, the age is valid
2.3.2 Returning multiple values using tuple packing
In Python, you return multiple values from a function by simply separating them with
commas in the return statement. Python automatically uses tuple packing to
group these values into a single tuple object, which is then returned.
How it Works
When the function is called, the returned tuple can be assigned to multiple variables
using tuple unpacking, making the code clean and readable.
1. Define the Function (Packing)
In the function definition, list the values you want to return, separated by commas.
Parentheses are optional but can improve readability.
python
def get_user_info():
name = "Alice"
age = 30
city = "New York"
# Python implicitly packs these into a tuple (name, age, city)
return name, age, city
# Or explicitly: return (name, age, city)
2. Call the Function (Unpacking)
When you call the function, assign the result to multiple variables (separated by
commas). Python automatically unpacks the values from the returned tuple into the
corresponding variables based on their position.
python
# Unpack the returned tuple into separate variables
user_name, user_age, user_city = get_user_info()

# Print the individual variables


print(f"Name: {user_name}")
print(f"Age: {user_age}")
print(f"City: {user_city}")
Output:
Name: Alice
Age: 30
City: New York

Key Points
 Implicit Packing: The comma operator, not the parentheses, is what creates
the tuple in Python.
 Matching Counts: The number of variables used for unpacking must match
the number of items in the returned tuple; otherwise, a ValueError will be
raised.

 Alternative Data Structures: For more complex data, or if you need


descriptive names for the returned values, you might consider returning a
dictionary, a dataclass, or a [Link] instead.

 Ignoring Values: You can use the underscore _ as a placeholder for values
you want to ignore during unpacking (e.g., name, _, city =
get_user_info() would ignore the age).

2.3.3 Use of return in conditional functions


In Python, using return within a conditional function is a common and Pythonic
practice used for two primary purposes: returning different values based on a
condition and exiting the function early (guard clauses).
Returning Different Values
Conditional statements (like if , elif , and else ) allow a function to evaluate
different scenarios and return a specific, relevant value for each case.
 Syntax:

python
def check_odd_even(number):
if number % 2 == 0:
return "Even"
else:
return "Odd"

result1 = check_odd_even(4) # result1 will be "Even"


result2 = check_odd_even(5) # result2 will be "Odd"
When a return statement is executed, the function's execution stops
immediately, and the specified value is sent back to the caller.
 Ternary Operator (Conditional Expression): For simple conditional returns,
you can use a single-line conditional expression, which is often considered
more concise and readable.
python
def check_odd_even_concise(number):
return "Even" if number % 2 == 0 else "Odd"

Exiting a Function Early (Guard Clauses)


You can use return statements at the beginning of a function to handle invalid
inputs or "edge cases" and exit immediately, preventing the rest of the function's
code from running unnecessarily. This pattern improves code readability by reducing
indentation and clearly defining the conditions under which the main logic should not
run.
 Example (Validation):

python
def process_value(input_number):
if not isinstance(input_number, (int, float)):
return "Invalid input" # Exits here if the type is wrong
if input_number < 0:
return "Negative number" # Exits here if number is negative

# Main logic starts only if the above conditions were not met
return "Positive or Zero"

Key Considerations
 Function Termination: A return statement immediately terminates the
function, so any code following it in the same block or general flow will not be
executed.
 Implicit None Return: If a function finishes executing without hitting an
explicit return statement, Python automatically returns the value None . This
can lead to bugs if the caller expects a different type of value, so all possible
execution paths should ideally return a value if one is expected.

 Multiple Values: A function can return multiple values by listing them after
the return statement (e.g., return value1, value2 ), which Python packs
into a single tuple.

2.3.4 Returning None and its implications


In Python, every function must return a value. If it does not have an
explicit return statement with a value, or if the function finishes execution without
reaching one, it implicitly returns the singleton object None . None is an object of
type NoneType and represents the absence of a value or a null value.
Why Functions Return None
A function returns None in the following scenarios:
 No return statement: The function performs actions (side effects like

printing to the console, modifying a list in place, etc.) but does not produce a
useful value for the caller.

python
def greet(name):
print(f"Hello, {name}!")

result = greet("Alice")
print(f"The return value is: {result}")
# Output:
# Hello, Alice!
# The return value is: None
 A bare return : Using return by itself is equivalent to return None . This
is often used for early exit from a function.

python
def check_value(x):
if x > 10:
return # Exits the function early
print(f"Value is {x}, which is not > 10.")

result = check_value(5)
print(f"The return value is: {result}")
# Output:
# Value is 5, which is not > 10.
# The return value is: None
 Conditional return paths missing a case: If a function
uses if / else statements and one logical path does not have an
explicit return statement, that path implicitly returns None , which can lead
to subtle bugs.

Implications of Returning None


 NoneType Errors: The most common implication is a AttributeError:
'NoneType' object has no attribute ... when you try to call a method
on a variable that is None . This often happens when the function you called
returned None unexpectedly.

 Boolean Context (Falsiness): None is considered "falsy" in a boolean


context (e.g., if value: evaluates to False if value is None ). This allows
for a concise check:

python
def find_item(item_list, target):
for item in item_list:
if item == target:
return item
return None # Explicitly indicate no result

found = find_item(['a', 'b', 'c'], 'z')


if found is not None: # Correct way to check if a valid value was
returned
print(f"Found item: {[Link]()}")
else:
print("Item not found.")
 API Design and Readability:
o Returning None is the Pythonic way to signal that a function has "no
result" or that an optional value is missing, similar to NULL in other
languages.

o Following the PEP 8 style guide, if a function returns an expression in


some cases, all other return paths (including the end of the function)
should explicitly return None for consistency and readability.

o Functions that perform "in-place" operations (like [Link]() )


return None to indicate they are executed for their side effects, not for
a useful return value.

 Use with Type Hinting: In modern Python, you can use type hints
like Optional[str] or str | None to explicitly inform static type checkers
(like Mypy) and other developers that a function might return a string or None ,
which helps catch potential errors early.
2.4 Passing Data to Functions
2.4.1 Passing simple data types
In Python, "simple" data types like integers ( int ), floating-point numbers ( float ),
and strings ( str ) are passed to functions by object reference, but they behave as
if they were passed by value due to their immutability. This means changes made to
the variable within the function do not affect the original variable outside the
function.
How Passing Simple Data Types Works
Python treats all data as objects. Simple types are immutable, meaning their values
cannot be changed after they are created.
 When you pass an immutable object (like an int , float , or str ) to a

function, the function receives a reference to that object.

 If you reassign the variable inside the function to a new value, a new object is
created in memory, and the local variable name inside the function points to
this new object. The original object outside the function remains unchanged.

Example: Integer ( int )


python
def update_number(num):
print(f"Inside function (before change): {num}")
num = num + 10 # Creates a new integer object, 'num' now points to it
print(f"Inside function (after change): {num}")

my_number = 5
print(f"Outside function (before call): {my_number}")
update_number(my_number)
print(f"Outside function (after call): {my_number}")
Output:
Outside function (before call): 5
Inside function (before change): 5
Inside function (after change): 15
Outside function (after call): 5
As shown above, my_number outside the function remains 5 because num = num
+ 10 created a new object.

Example: String ( str )


python
def update_greeting(text):
print(f"Inside function (before change): {text}")
text = text + ", how are you?" # Creates a new string object
print(f"Inside function (after change): {text}")

greeting = "Hello"
print(f"Outside function (before call): {greeting}")
update_greeting(greeting)
print(f"Outside function (after call): {greeting}")
Output:
Outside function (before call): Hello
Inside function (before change): Hello
Inside function (after change): Hello, how are you?
Outside function (after call): Hello

Key Simple Data Types in Python


Data Type Class Description Mutability

Integer int Whole numbers (e.g., 10 ) Immutable

Float float Numbers with decimals (e.g., 10.5 ) Immutable

String str Text wrapped in quotes (e.g., "Hello" ) Immutable

Boolean bool Logical values ( True or False ) Immutable

2.4.2 Passing a list to a function


In Python, you pass a list to a function just like any other variable: you define the
function to accept a parameter and then call the function with the list as the
argument.
Basic Example
Here is a simple example that iterates through a list passed to a function:
python
# Function definition that accepts a list as an argument (parameter name
'food')
def print_list_items(food):
for item in food:
print(item)

# Create a list
fruits = ["apple", "banana", "cherry"]
# Call the function, passing the list as an argument
print_list_items(fruits)
Output:
apple
banana
cherry

Key Considerations
When passing a list to a function, it's important to understand how Python handles
mutable objects like lists.
 Pass-by-Object-Reference: Python uses a system often described as "pass-
by-object-reference" or "pass-by-assignment".

 Mutability: Lists are mutable objects. This means that if you modify the
list inside the function (e.g., using append() , remove() , or sort() ), the
original list outside the function will also be changed because both the original
variable name and the function parameter name refer to the same object in
memory.

Example of modifying the original list:


python
def add_item(my_list):
my_list.append("orange") # Modifies the list in-place
print(f"Inside function: {my_list}")

items = ["apple", "banana"]


add_item(items)
print(f"Outside function: {items}")
Output:
Inside function: ['apple', 'banana', 'orange']
Outside function: ['apple', 'banana', 'orange']

Preventing Modification of the Original List


If you want the function to work with a copy of the list and leave the original
unchanged, you should pass a copy of the list to the function.
You can create a shallow copy using slicing [:] or the list() constructor.
Example using a shallow copy:
python
def add_item_to_copy(my_list_copy):
my_list_copy.append("orange")
print(f"Inside function (copy): {my_list_copy}")
items = ["apple", "banana"]
# Pass a shallow copy using the slicing operator [:]
add_item_to_copy(items[:])
print(f"Outside function (original): {items}")
Output:
Inside function (copy): ['apple', 'banana', 'orange']
Outside function (original): ['apple', 'banana']

Passing List Elements as Separate Arguments


If you have a function that expects multiple individual arguments and you want to
pass the elements of a list to it, you can use the unpacking operator * when
calling the function.
python
def introduce(first_name, last_name):
print(f"Hi, I am {first_name} {last_name}.")

names = ["Jane", "Doe"]

# Unpack the list into separate arguments


introduce(*names)
Output:
Hi, I am Jane Doe.

2.4.3 Modifying lists inside functions


In Python, lists are mutable objects and are passed to functions by object
reference. This means that if you perform mutable operations on a list inside a
function, the original list outside the function will be modified.
How to Modify the Original List
When you use in-place methods or indexing, the changes affect the original list
object:
 Using list methods: Methods
like append() , extend() , insert() , remove() , pop() ,
and sort() modify the list in place.

python
def modify_list_inplace(my_list):
my_list.append(4)
my_list.remove(2)
my_list[0] = 100

original_list = [1, 2, 3]
modify_list_inplace(original_list)
print(original_list)
# Output: [100, 3, 4]
 Using slice assignment: This method replaces the entire content of the
original list with new elements.

python
def replace_list_contents(my_list):
my_list[:] = [10, 20, 30]

original_list = [1, 2, 3]
replace_list_contents(original_list)
print(original_list)
# Output: [10, 20, 30]

Creating a New List vs. Modifying In-Place


It is important to understand the difference between modifying a list in-place and
creating a new list.
Reassigning the Variable Name (Does not modify the original)
If you use the assignment operator ( = ) on the list variable inside the function, you
are creating a new local variable in the function's scope that points to a new list
object. This does not affect the original list outside the function's scope.
python
def reassign_variable(my_list):
my_list = [10, 20, 30] # Creates a new local list
print(f"Inside function: {my_list}")

original_list = [1, 2, 3]
reassign_variable(original_list)
print(f"Outside function: {original_list}")
# Output:
# Inside function: [10, 20, 30]
# Outside function: [1, 2, 3]
The Pythonic Way: Functions that modify vs. functions that return a new list
A common "Pythonic" convention is to name functions to reflect their behavior:
 Functions that modify an object in place often have a command-like name
(e.g., [Link]() ) and return None .
 Functions that return a new object (leaving the original unchanged) use
descriptive names (e.g., sorted(list) ) and return the new value.

python
# Modifies in-place, returns None
items = [4, 3, 10, 2, 5]
[Link]()
print(items) # Output: [2, 3, 4, 5, 10]

# Creates a new list, original is unchanged


items = [4, 3, 10, 2, 5]
new_items = sorted(items)
print(items) # Output: [4, 3, 10, 2, 5]
print(new_items) # Output: [2, 3, 4, 5, 10]

Preventing Modification (Working with a Copy)


If you want to ensure the original list is not changed, you can pass a copy of the list
to the function using slicing or the copy() method.
python
def no_modification(my_list):
# Works on a local copy, original list is safe
local_list = my_list.copy()
local_list.append(100)
print(f"Inside function: {local_list}")

original_list = [1, 2, 3]
no_modification(original_list)
print(f"Outside function: {original_list}")
# Output:
# Inside function: [1, 2, 3, 100]
# Outside function: [1, 2, 3]

2.4.4 Passing dictionaries and tuples


In Python, you pass lists, tuples, and dictionaries to functions by object reference.
This means the function receives a reference to the same object in memory that the
caller is using. The primary difference in behavior depends on whether the object
is mutable (lists, dictionaries) or immutable (tuples).
Passing Lists
Lists are mutable, so any modifications made to the list within the function will affect
the original list outside the function's scope.
python
def modify_list(my_list):
"""Appends an item to the list and modifies an existing item."""
my_list.append("kiwi") # Modifies the original list
my_list[0] = "strawberry" # Modifies the original list
print(f"Inside function: {my_list}")

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


modify_list(fruits)
print(f"Outside function: {fruits}")

# Output:
# Inside function: ['strawberry', 'banana', 'cherry', 'kiwi']
# Outside function: ['strawberry', 'banana', 'cherry', 'kiwi']
If you want to pass a list to a function and prevent the original from being modified,
you should pass a copy of the list, such as by using slicing [:] or
the copy() method.
python
def no_side_effects(my_list):
"""Modifications only affect the local copy."""
my_list.append("grape")
print(f"Inside function: {my_list}")

original_fruits = ["apple", "banana"]


no_side_effects(original_fruits[:]) # Pass a shallow copy
print(f"Outside function: {original_fruits}")

# Output:
# Inside function: ['apple', 'banana', 'grape']
# Outside function: ['apple', 'banana']

Passing Tuples
Tuples are immutable, so you cannot change their elements in-place. While the
function receives a reference to the tuple, any operation that appears to "modify" the
tuple actually creates a new tuple object within the function's local scope, leaving the
original tuple unchanged.
python
def try_to_modify_tuple(my_tuple):
"""Tries to modify a tuple (will fail) and reassigns the variable
(creates a new object)."""
try:
my_tuple[0] = "new value"
except TypeError as e:
print(f"Can't modify tuple elements: {e}")

# Reassigning the variable creates a new local object


my_tuple = my_tuple + ("new_item",)
print(f"Inside function (reassigned): {my_tuple}")

my_data = ("Alice", 30, "USA")


try_to_modify_tuple(my_data)
print(f"Outside function: {my_data}")

# Output:
# Can't modify tuple elements: 'tuple' object does not support item
assignment
# Inside function (reassigned): ('Alice', 30, 'USA', 'new_item')
# Outside function: ('Alice', 30, 'USA')

Passing Dictionaries
Dictionaries are mutable, behaving similarly to lists. Changes made within the
function will persist outside of it.
python
def update_age(person_dict):
"""Updates a value in the dictionary."""
person_dict["age"] = 31 # Modifies the original dictionary
person_dict["city"] = "New York" # Adds a new key-value pair
print(f"Inside function: {person_dict}")

person_info = {"name": "Alice", "age": 30}


update_age(person_info)
print(f"Outside function: {person_info}")

# Output:
# Inside function: {'name': 'Alice', 'age': 31, 'city': 'New York'}
# Outside function: {'name': 'Alice', 'age': 31, 'city': 'New York'}

Unpacking Arguments
You can also unpack these data structures using the * (for lists and tuples)
and ** (for dictionaries) operators when calling a function, which passes their
elements as individual positional or keyword arguments.
python
def greet(first_name, last_name, title):
print(f"Hello, {title} {first_name} {last_name}")

# Unpack a list/tuple into positional arguments


name_list = ["John", "Doe", "Mr."]
greet(*name_list)

# Unpack a dictionary into keyword arguments


name_dict = {"first_name": "Jane", "last_name": "Smith", "title": "Ms."}
greet(**name_dict)

# Output:
# Hello, Mr. John Doe
# Hello, Ms. Jane Smith

2.4.5 Mutable vs. Immutable data types


In Python, mutable data types can be modified after they are created,
while immutable data types cannot be changed once defined. This difference affects
how they are stored and managed in memory.
Mutable Data Types
Mutable objects can have their value or internal state changed without creating a
new object in memory. Their memory address (identity) remains the same after
modification.
Common mutable data types include:
 Lists: Ordered collections that allow adding, removing, or changing elements
in place (e.g., my_list[0] = 10 ).

 Dictionaries: Unordered collections of key-value pairs where items can be


added, updated, or removed dynamically.

 Sets: Unordered collections of unique elements that allow adding or removing


items.

 Byte Arrays: Mutable sequences of bytes.

Immutable Data Types


Immutable objects cannot be altered after their creation. Any operation that appears
to modify an immutable object actually creates a new object in memory, and the
variable is then reassigned to this new object.
Common immutable data types include:
 Integers, Floats, and Complex numbers.
 Strings: Sequences of characters where individual characters cannot be
changed in place (e.g., my_string[0] = 'H' would raise an error).

 Tuples: Ordered collections, similar to lists, but their elements cannot be


modified, added, or removed after creation.

 Frozensets: Immutable versions of sets.

 Bytes: Immutable sequences of bytes.

Key Differences
Feature Mutable Data Types Immutable Data Types

Modificatio Can be changed after creation. Cannot be changed after creation.


n

Memory ID ID (memory address) remains the ID changes when a new value is assigned,
same when modified. as a new object is created.

Use Cases Ideal for dynamic data that requires Ideal for data that should remain constant
frequent updates (e.g., shopping cart (e.g., dictionary keys, constants, thread-safe
items). data).

Hashability Not hashable (cannot be used as Hashable (can be used as dictionary keys or
dictionary keys or set elements). set elements).

2.5 Creating and Using Classes


2.5.1 Introduction to Object-Oriented Programming
Object-Oriented Programming (OOP) is a software design model that organizes
code around objects (data + methods) instead of functions, modeling real-world
entities for modular, reusable, and maintainable applications, built on core principles
like Encapsulation, Abstraction, Inheritance, and Polymorphism. It
uses classes (blueprints) to create objects (instances) that interact, simplifying
complex systems by bundling data and behavior together.
Core Concepts
 Object: An instance of a class, combining related data (attributes/properties)
and functions (methods/behavior) into a single unit.
 Class: A blueprint or template that defines the properties and methods all
objects of that type will have.

2.5.2 Creating a class using `class` keyword


The class keyword is used in several programming languages (including Python,
C++, and Java) to define a new class, which serves as a blueprint for creating
objects.
General Syntax
The basic syntax involves using the class keyword followed by the desired name
for the class.
Python
In Python, the syntax is straightforward, using a colon : instead of braces {} and
an indentation-based structure. An __init__() method acts as the constructor to
initialize object attributes.
python
class ClassName:
# The __init__ method (constructor) initializes the instance attributes
def __init__(self, attribute1, attribute2):
self.attribute1 = attribute1
self.attribute2 = attribute2

# Other methods can be defined within the class


def some_method(self):
print(f"Value 1: {self.attribute1}")

# Creating an object (instance) of the class


my_object = ClassName("value_a", "value_b")
my_object.some_method()

2.5.3 The `__init__()` constructor method


The __init__() method in Python is a special method that is automatically called
when a new instance (object) of a class is created. It is primarily used to initialize
the attributes of the newly created object to a desired starting state.
Key Concepts
 Initializer, Not the Primary Constructor: While often referred to as a
"constructor" in common usage due to its role in object setup, the technical
constructor in Python is actually the __new__() method, which creates the
instance itself. __init__() is an initializer that runs immediately
after __new__() to set initial attribute values.

 Automatic Invocation: You don't call __init__() explicitly in your code;


Python handles the call automatically when you instantiate a class.
 The self Parameter: The __init__() method (and all instance methods)
must take at least one argument, which by convention is named self . This
parameter refers to the newly created instance of the object itself, allowing
you to access and set its attributes (e.g., [Link] = name ).

 Purpose: The main uses include:

o Assigning values to instance variables.

o Performing setup operations necessary for the object to be ready for


use (e.g., opening a file, connecting to a database).

o Calling the initializer of the parent class when using inheritance


( super().__init__() ) to ensure base class attributes are also
initialized.

Example
python
class Dog:
# The __init__ method initializes the attributes for a new Dog object
def __init__(self, name, age):
[Link] = name # Assigns the 'name' argument to the object's
'name' attribute
[Link] = age # Assigns the 'age' argument to the object's
'age' attribute

def bark(self):
print(f"{[Link]} says Woof!")

# Creating a new instance of the Dog class automatically calls __init__


my_dog = Dog("Buddy", 3)

# The object is now initialized and ready to use


print(f"My dog's name is {my_dog.name} and he is {my_dog.age} years old.")
my_dog.bark()
Output:
My dog's name is Buddy and he is 3 years old.
Buddy says Woof!

2.5.4 Creating objects (instances) of a class


Creating an object (or "instantiating a class") involves using the class's blueprint to
build a concrete, unique entity in the computer's memory. This is typically done using
a specific syntax depending on the programming language, often involving the class
name and parentheses, and sometimes a new keyword.
General Process
The general steps for creating an object are:
1. Define a class: First, a class (the blueprint) must be defined, specifying its
attributes (data) and methods (behaviors).

2. Instantiate the class: This is the process of creating the actual object from
the class blueprint. Memory is allocated for the new object's unique data.

3. Initialize the object: During instantiation, a special method called


a constructor is automatically called to set the initial state (values of
attributes) of the new object.

Examples in Different Languages


The exact syntax for creating an object varies by language:
Language Syntax for creating an object Key points
named myObj from a
class MyClass

Python myObj = MyClass() Python does not use a new keyword.


The __init__ method acts as the constructor and
is called automatically when the class is called with
parentheses.

Java MyClass myObj = new The new keyword is used to allocate memory and
MyClass(); invoke the constructor. The class name is used as
the type of the variable and for the constructor call.

C++ MyClass myObj; An object can be created by simply declaring a


variable of the class type. The constructor is called
automatically
2.5.5 Instance variables and methods
Instance variables and methods are members of a class that belong to specific object instances
rather than the class itself. Defined outside methods, instance variables represent an object's
state, with each instance holding its own, independent copy. Instance methods act on
these variables to define object behavior, requiring an object instantiation to be invoked.

Key Characteristics
 Definition & Scope: Instance variables are defined inside a class but outside any
method or constructor.

 Storage: They are stored in the heap memory.

 Independence: Each object (instance) has its own copy of these variables; changing
a variable in one object does not affect another.

 Lifespan: They are created when an object is instantiated and destroyed when the
object is destroyed.

 Access: Instance methods can directly access instance variables and other methods
within the same class.

Instance Variables vs. Instance Methods


Feature Instance Variables Instance Methods

Purpose Store data/state specific to an Perform operations/behavior on object data.


object.

Declaration Inside class, outside methods. Inside class, defined without 'static' keyword.

Access Accessed via object reference Called via object reference


(e.g., [Link] ). (e.g., [Link]() ).

Examples name , age , balance . deposit() , withdraw() , getName() .

2.5.6 Class vs. Instance variables


The key difference is that instance variables are unique to each object,
while class variables are shared by all instances of a class.
Class Variables
 Ownership: Owned by the class itself, not any specific object.

 Sharing: There is only a single copy of a class variable, which is shared


among all instances of the class.

 Declaration: Declared directly within the class body (outside of any methods,
typically before the constructor).

 Access: Can be accessed using either the class name or an object reference,
but modifying through the class name is recommended to avoid confusion
with the creation of a new instance variable.

 Use Cases: Ideal for storing data that is consistent across all instances, such
as constants, default settings, or a counter for the number of objects created.

Instance Variables
 Ownership: Owned by individual instances (objects) of the class.

 Sharing: Each object gets its own unique copy of the instance variable, and
changes to one object's copy do not affect any others.

 Declaration: Typically defined within the class's constructor


(e.g., __init__ in Python) or other instance methods, using an object
reference like self or this .

 Access: Accessed using an object (instance) reference.

 Use Cases: Used to store the unique state or properties of each object, such
as a person's name or age.

Summary Table
Feature Instance Variable Class Variable

Ownership Each object has its own copy A single copy is shared by all objects

Declaration Inside methods/constructor Directly inside the class body (outside


(using self / this ) methods)

Access Via object reference Via class name ( [Link] ) or


Method ( [Link] ) object reference
Lifetime Exists as long as the object exists Exists for the lifetime of the program/class
load

Typical Use Unique object state (e.g., name, Shared data, constants (e.g., species
balance) name, max speed)

2.5.7 Encapsulation and basic data hiding


In Python, encapsulation is the practice of bundling data (attributes) and methods
that operate on that data into a single unit (a class), while data hiding is a technique
used within encapsulation to restrict direct access to an object's internal state.
Unlike some other object-oriented programming languages, Python does not have
strict "private" or "protected" keywords but uses naming conventions to achieve
these concepts.
Encapsulation in Python
Encapsulation's primary goal is to hide the internal implementation details and
complexity of a class from the outside world, exposing only a well-defined public
interface. This improves modularity, security, and maintainability.
Data Hiding in Python
Data hiding is achieved through access modifiers implemented as naming
conventions:
 Public members: By default, all members (variables and methods) in a
Python class are public. They can be accessed from anywhere inside or
outside the class and are defined without any underscore prefix
(e.g., [Link] ).

 Protected members: These are intended for use within the class and its
subclasses. The convention is to prefix the member name with a single
underscore (e.g., self._name ). Python does not strictly enforce this, but it
serves as a strong signal to programmers that the member is for internal use.

 Private members: These members are accessible only within the class
where they are defined. The convention is to prefix the member name with
a double underscore (e.g., self.__salary ).

Name Mangling
Python achieves a form of data hiding for private members through a mechanism
called name mangling. The interpreter automatically renames the private attribute
internally to _ClassName__attributeName . This makes it harder (though not
impossible) to access the variable directly from outside the class, preventing
accidental modification.
Controlled Access (Getters/Setters)
To interact with private or protected data in a controlled and safe manner, "getter"
and "setter" methods are often used. These methods provide a public interface to
read or update the data, allowing for validation or other logic before the data is
changed.
python
class Employee:
def __init__(self, name, salary):
[Link] = name # Public attribute
self.__salary = salary # Private attribute

# Getter method
def get_salary(self):
return self.__salary

# Setter method with validation


def set_salary(self, amount):
if amount > 0:
self.__salary = amount
else:
print("Invalid salary amount!")

# Usage
emp = Employee("Fedrick", 50000)

print(f"Employee Name: {[Link]}")


print(f"Employee Salary: {emp.get_salary()}") # Access via getter

emp.set_salary(60000) # Update via setter


print(f"New Salary: {emp.get_salary()}")

# Attempting direct access will cause an AttributeError (due to name


mangling)
try:
print(emp.__salary)
except AttributeError as e:
print(f"Error accessing private attribute directly: {e}")

Summary of Differences
Feature Encapsulation Data Hiding

Scope Bundling data and methods into a single unit Restricting access to internal state
(class). (variables).

Objective Reduce complexity and increase modularity. Enhance security and maintain
data integrity.

Relationshi Data hiding is a core principle or a subset of The result of using encapsulation
p the broader concept of encapsulation. techniques to protect data

2.6 Strings and String Methods


In Python, a string is an immutable sequence of Unicode characters used to
represent text. String methods are built-in functions that perform operations on
strings and return a new string, as the original cannot be changed.
Key Characteristics of Strings
 Definition: Strings are enclosed in single quotes ( '...' ), double quotes
( "..." ), or triple quotes ( """...""" for multiline strings).

 Immutability: Once a string is created, you cannot change its individual


characters. Operations like modification or concatenation create a new string
object.

 Indexing and Slicing: Characters can be accessed using zero-based


indexing (e.g., my_string[0] gives the first character). Slicing
( my_string[start:end] ) extracts substrings.

 No Character Type: Python does not have a separate character type; a


single character is treated as a string of length one.

Common String Methods


Python offers a rich set of built-in methods for string manipulation. The following
table summarizes some of the most useful ones:
Method Description Example

upper() Converts all "hello".upper() returns "HELLO"


characters to
uppercase.
lower() Converts all "WORLD".lower() returns "world"
characters to
lowercase.

strip() Removes leading and " hello ".strip() returns "hello"


trailing whitespace.

replace(old, Replaces occurrences "dog".replace("d", "f") returns "fog"


new) of a substring.

split(sep) Splits the string into a "a,b,c".split(",") returns ['a', 'b',


list of substrings using 'c']
a delimiter ( sep ). If
no separator is given,
it splits on all
whitespace.

join(iterable) Concatenates ", ".join(['a', 'b', 'c']) returns "a,


elements of an b, c"
iterable (e.g., a list)
into a single string,
using the string as the
separator.

find(sub) Returns the lowest "hello".find("l") returns 2


index where the
substring is found, or -
1 if not found.

count(sub) Returns the number of "banana".count("a") returns 3


occurrences of a
substring.

startswith(pref Returns True if the "[Link]".startswith("image") returns


ix) string starts with the True
specified
prefix, False otherw
ise.
endswith(suffix Returns True if the "[Link]".endswith(".png") returns Tr
) string ends with the ue
specified
suffix, False otherwi
se.

2.6.1 Creating and manipulating strings


In Python, strings are an immutable sequence of characters used for handling text
data. Any operation that appears to modify a string actually creates a new string
object in memory.
Creating Strings
Strings can be created using single, double, or triple quotes. Triple quotes are
generally used for multi-line strings.

python
# Single quotes
single_quote_str = 'Hello, world!'

# Double quotes
double_quote_str = "Hello, world!"

# Triple quotes for multi-line strings (preserves line breaks and


indentation)
multi_line_str = """This is a
multi-line string."""
You can also use the built-in str() function to convert other data types into strings.
Basic String Manipulation
Concatenation and Repetition
You can combine or repeat strings using the + and * operators.
python
greeting = "Hello"
name = "Pythonista"

# Concatenation
full_greeting = greeting + ", " + name + "!" # Output: Hello, Pythonista!

# Repetition
laugh = "ha" * 3 # Output: hahaha
Indexing and Slicing
Characters in a string can be accessed using zero-based indexing within square
brackets ( [] ). Slicing extracts a portion of a string.
python
s = "Hello World"

# Accessing individual characters (indexing)


first_char = s[0] # 'H'
last_char = s[-1] # 'd' (negative indices count from the end)

# Extracting substrings (slicing)


substring = s[1:5] # 'ello' (starts at index 1, ends before index 5)
sub_end = s[6:] # 'World' (from index 6 to the end)
sub_start = s[:5] # 'Hello' (from the beginning to index 5)

# Reversing a string using slicing with a step of -1


reversed_s = s[::-1] # 'dlroW olleH'

String Formatting
F-strings (formatted string literals) are the recommended way to embed variables or
expressions within a string in Python 3.6+.
python
name = "Alice"
age = 30
message = f"My name is {name} and I am {age} years old."
# Output: My name is Alice and I am 30 years old.

Common String Methods


Python's built-in str class provides numerous methods for manipulation. They do
not change the original string but return a new one with the modifications.
Method Description Example

len() Returns the length of the string len("Python") -> 6

.lower() Converts string to lowercase "HELLO".lower() -> "hello"

.upper() Converts string to uppercase "hello".upper() -> "HELLO"

.strip() Removes leading/trailing whitespace " Hello ".strip() -> "Hello"


.replace() Replaces occurrences of a substring "dog".replace("d", "f") -
> "fog"

.split() Splits string into a list by a delimiter "a,b,c".split(",") -> ['a',


'b', 'c']

.join() Joins elements of an iterable (list) into a "-".join(["2025", "05"]) -


string using a separator > "2025-05"

.find() Returns the index of the first occurrence of


a substring, or -1 if not found

2.6.2 String concatenation and formatting


Python offers several methods for string concatenation and formatting, with
modern f-strings being the recommended approach for most use cases due to
their readability and performance. Other methods like the + operator
and join() are useful in specific scenarios.
String Concatenation Methods
Concatenation is the process of joining two or more strings end-to-end to create a
new, single string.
Method Description Best Use Case

+ Operator Simple, intuitive operator to merge strings. Combining a small, fixed number of
strings.

[Link]() Concatenates elements of an iterable (e.g., a Efficiently combining large


list) with a specified separator string. numbers of strings, especially in
loops.

String Placing two or more string literals next to each Splitting long strings across
Literals other automatically concatenates them. multiple lines in the source code.

python
# Using the + operator
first = "John"
last = "Doe"
full_name = first + " " + last
print(full_name) # Output: John Doe
# Using [Link]()
words = ["Python", "is", "efficient"]
result = " ".join(words)
print(result) # Output: Python is efficient

# Note: The + operator requires all operands to be strings. To combine


strings and numbers, you must first convert the number to a string using
str().
# Example: message = "I am " + str(25) + " years old."

String Formatting Methods


String formatting allows you to create dynamic text by inserting values into
placeholders within a string.

Method Description

f-strings (Formatted (Python 3.6+) The most modern and recommended method. Prefix the
String Literals) string with f and embed variables or expressions inside curly
braces {} .

[Link]() A flexible and powerful method that uses {} as placeholders, which are
then replaced by values passed to the method.

% Operator An older, "C-style" method using type specifiers like %s (string)


and %d (integer). It is generally discouraged for new code.

python
name = "Alice"
age = 30

# Using f-strings (Recommended for Python 3.6+)


greeting_f = f"My name is {name} and I am {age} years old."
print(greeting_f) # Output: My name is Alice and I am 30 years old.

# Using the .format() method (compatible with older Python versions)


greeting_format = "My name is {} and I am {} years old.".format(name, age)
print(greeting_format) # Output: My name is Alice and I am 30 years old.
# Using the % operator (Legacy method)
greeting_percent = "My name is %s and I am %d years old." % (name, age)
print(greeting_percent) # Output: My name is Alice and I am 30 years old.

2.6.3 String slicing and indexing


String indexing is used to access a single character by its position,
while slicing extracts a sequence of characters (a substring) using a range of
indices. Both use square brackets [] with integer indices and support both positive
(forward) and negative (backward) indexing.
String Indexing
In Python, strings are ordered sequences, and each character has a corresponding
index. Indexing is zero-based, meaning the first character is at index 0 .

Syntax Description Example Output

string[index] Access a single character s = "Python" P


print(s[0])

string[-index] Access from the end s = "Python" n


print(s[-1])

Attempting to access an index that is out of range will raise an IndexError .


String Slicing
Slicing allows you to extract a portion of a string. The syntax
is string[start:stop:step] .
 start : The starting index (inclusive). Defaults to 0 if omitted.

 stop : The ending index (exclusive). The character at this index

is not included in the result. Defaults to the end of the string if omitted.
 step : The interval between characters (stride). Defaults to 1 if omitted.

Syntax Description Example Output

s[start:stop] Characters from start to stop-1 s = "Hello Hello


World"
print(s[0:5])
s[start:] Characters from start to the end s = "Hello World
World"
print(s[6:])

s[:stop] Characters from the beginning to stop- s = "Hello Hello


1 World"
print(s[:5])

s[::step] Every step -th character in the string s = "SmySak" SmySak


print(s[::2])

s[::-1] Reverses the string s = "Python" nohtyP


print(s[::-1])

Key Differences
Aspect Indexing Slicing

Returns A single character A new substring

Out of bounds Raises an IndexError Returns an empty or partial string gracefully

Syntax string[index] string[start:stop:step]

2.6.4 Useful string methods (`upper()`, `lower()`, `strip()`, `find()`,


`replace()`, etc.)
String methods are powerful tools for manipulating text data in programming
languages like Python. Here are descriptions and examples of some of the most
useful methods:
Method Description Example

upper() Converts all "hello".upper() returns "HELLO" .


characters in a
string to
uppercase.

lower() Converts all "WORLD".lower() returns "world" .


characters in a
string to lowercase.

strip() Removes leading " Gfg ".strip() returns "Gfg" .


and trailing
whitespace (or
specified
characters) from a
string.

find() Searches for a "Python".find("th") returns 2 .


specified substring
and returns the
index of the first
occurrence.
Returns -1 if not
found.

replace( Replaces all "Python is fun".replace("fun",


) occurrences of a "awesome") returns "Python is awesome" .
specified substring
with another string.

len() While not a len("hello") returns 5 .


method,
the len() functio
n returns the length
(number of
characters) of the
string.

split() Splits a string into "apple,banana,cherry".split(",") returns ['apple


a list of substrings ', 'banana', 'cherry'] .
based on a
specified delimiter.

count() Returns the


number of times a
specified substring
appears in the
string.

2.6.5 String testing methods (`isalpha()`, `isdigit()`, `islower()`, etc.)


String testing methods, common in programming languages like Python, are built-in
functions used to check the content and properties of a string. These methods
typically return a boolean value ( True or False ) based on whether all characters in
the string meet a specific condition.
Common String Testing Methods (Python)
Method Description Example

isalnum() Returns True if all characters in 'abc123'.isalnum() -> True


the string are alphanumeric
(letters or numbers).

isalpha() Returns True if all characters 'hello'.isalpha() -> True


are letters of the alphabet (no
spaces, numbers, or symbols).

isdigit() Returns True if all characters '12345'.isdigit() -> True


are digits (0-9).

islower() Returns True if all cased 'hello'.islower() -> True


characters in the string are
lowercase.

isupper() Returns True if all cased 'HELLO'.isupper() -> True


characters in the string are
uppercase.

isspace() Returns True if all characters ' \\n\\t '.isspace() -> True
are whitespace characters (e.g.,
space, tab, newline).

istitle() Returns True if the string follows 'Hello World'.istitle() -


the rules of a title (first letter of > True
each word is uppercase, rest are
lowercase).

isidentifier() Returns True if the string is a 'my_variable'.isidentifier() ->


valid identifier (variable name) in True
the language.

Key Characteristics
 Purpose: These methods are useful for data validation, such as checking if
user input for a password contains both letters and numbers, or if an age field
only contains digits.
 Behavior: They typically return True only if all characters in the string satisfy
the condition. If any character does not meet the criteria, they return False .

 Language Specificity: While similar functions exist in other languages like


C/C++ (e.g., isalpha() , isdigit() are found in <ctype.h> header), the
exact names, behavior with special characters or empty strings, and
implementation details are specific to the programming language. The
examples above are for Python.
 Empty Strings: For many of these methods (except perhaps isspace() ), an
empty string will typically return False because it does not contain any
characters that meet the condition (e.g., there are no alphabetic characters in
an empty string).
 Case Sensitivity: Methods like islower() and isupper() only check
cased characters; non-alphabetic characters (like numbers or symbols) are
ignored in this evaluation.

2.6.6 Immutability of strings


In Python, strings are immutable, which means their content cannot be changed
after they are created. Any operation that appears to modify a string, such as
concatenation or using string methods (e.g., .upper() or .replace() ), actually
creates a new string object in memory, and the original variable is then updated to
point to this new object.
Key Characteristics of String Immutability
 No In-Place Modification: You cannot change individual characters of a
string using item assignment. Attempting to do so will raise a TypeError .

python
s = "hello"
# s[0] = "H" # This line raises a TypeError: 'str' object does not
support item assignment
 New Objects are Created: When you perform operations like concatenation,
a new string is generated.

python
s1 = "hello"
# Check the memory address of the original string
print(id(s1))

s1 += " world"
# The new s1 points to a different memory address
print(id(s1))
 Methods Return New Strings: All built-in string methods, such
as .strip() , .lower() , or .replace() , return a new string with the
modifications applied, leaving the original string untouched.

python
user_name = " Alice "
cleaned_name = user_name.strip() # original user_name is unchanged
print(user_name) # Output: " Alice "
print(cleaned_name) # Output: "Alice"

Why are Strings Immutable?


Immutability offers several important benefits in Python programming:
 Data Integrity: It prevents a string's value from being unexpectedly altered by
another part of the program, making code more predictable and easier to
debug.

 Thread Safety: Immutable objects are inherently thread-safe because their


state cannot be changed, which removes the need for complex
synchronization mechanisms in multi-threaded environments.

 Hashability: Because their value is constant, strings can be hashed (given a


unique identifier) and used reliably as keys in dictionaries or elements in sets.
 Memory Efficiency: Python can optimize memory usage by "interning"
(reusing) identical string literals.

Working with "Mutable" Text


If you need to perform frequent, in-place character modifications, you can use
mutable alternatives like a list of characters or a bytearray :
python
s = "Hello"
char_list = list(s) # Convert the string to a list of characters
char_list[0] = 'J' # Modify the list in-place
new_s = "".join(char_list) # Join the list back into a new string
print(new_s) # Output: "Jello"

2.6.7 Escape sequences and raw strings


In Python, escape sequences use the backslash ( \ ) to insert special or non-
printable characters into a string, while raw strings, prefixed with r or R , treat
backslashes as literal characters, ignoring escape sequences.

Escape Sequences
An escape sequence is a combination of characters starting with a backslash that
represents a single special character. They are used for characters that are difficult
to type or would otherwise break the string's syntax, like quotes or newlines.
Escape Meaning Example Output
Sequence

\' Single quote print('It\\\'s a great day!') It's a


great day!

\" Double quote print("This is a \\"quote\\".") This is a


"quote".

\\\\ Backslash print('This \\\\ is a backslash.') This \ is a


backslash.

\\n Newline (line print('Line 1\\nLine 2') Line 1


feed) Line 2

\\t Horizontal print('Name:\\tAge') Name: Age


Tab

\\r Carriage print('Hello\\rWorld') World


Return

\\b Backspace print('Hello\\bWorld') HellWorld

\\x hh Hexadecimal print('\\x48\\x65\\x6c\\x6c\\x6f') Hello


value

\\ ooo Octal value print('\\101') A

\\N{name} Named print('\\N{SNAKE}') 🐍


Unicode
character

\\uxxxx 16-bit print('\\u0041') A


Unicode
value

\\Uxxxxxxxx 32-bit print('\\U0001F600') 😀


Unicode
value

Raw Strings
Raw strings are created by prefixing a string literal with the letter r or R (e.g., r"my
string" or R'my string' ). The interpreter treats backslashes in raw strings as

literal characters, not as the start of an escape sequence.


Key characteristics and uses:
 Literal interpretation: r"Hello\\nWorld" prints as Hello\nWorld ,
whereas "Hello\\nWorld" prints with a newline.

 Useful for file paths: Windows paths use backslashes as separators, which
can be problematic in regular strings (e.g., "C:\\Users\\Name" requires
double backslashes). Raw strings simplify this: r"C:\Users\Name" .
 Useful for regular expressions: Regex patterns heavily use backslashes.
Using raw strings, like r"\\bword\\b" , avoids the "leaning toothpick
syndrome" of excessive backslash escaping in normal strings.

 Limitation: A raw string cannot end with an odd number of backslashes, as


the last backslash would escape the closing quote, leading to
a SyntaxError .

Example of difference:
python
# Normal string: \n is interpreted as a newline
print("File path: C:\\Users\\Name\\[Link]\\nNew line here.")

# Raw string: \n is treated as the literal characters '\' and 'n'


print(r"File path: C:\Users\Name\[Link]\nNo new line here.")
Output:
File path: C:\Users\Name\[Link]
New line here.
File path: C:\Users\Name\[Link]\nNo new line here.

2.7 Working with Files


2.7.1 File handling basics in Python
File handling in Python is the process of managing data flow to and from files stored
permanently on a computer's storage. The core process involves opening a file,
performing operations (reading, writing, appending), and closing it.
The open() Function and File Modes
The built-in open() function is the primary way to interact with files. It takes the file
path and a mode as arguments and returns a file object (or handle).
python
file_object = open("[Link]", "mode")
Common file modes include:
Mode Description Action if File Exists Action if File Does Not Exist
'r' Read Cursor at the beginning FileNotFoundError raised
(default)

'w' Write Overwrites the entire file Creates a new file

'a' Append Cursor at the end Creates a new file

'x' Exclusive FileExistsError raised Creates a new file


creation

'b' Binary Used with other modes


(e.g., 'rb' , 'wb' ) for non-text files like
images or executables

Best Practice: Using the with Statement


The most robust and recommended way to handle files in Python is using
the with statement. This ensures the file is automatically closed, even if errors
occur, preventing data loss or resource leaks.
python
with open("[Link]", "r") as file:
# Operations go here
content = [Link]()
# File is automatically closed outside the 'with' block

Reading from a File


Once a file is opened in a read-compatible mode, you can use several methods to
read its content:
 read(size) : Reads the entire file as a single string. The
optional size argument limits the number of characters/bytes read.

 readline() : Reads a single line from the file at a time.

 readlines() : Reads all lines of the file and returns them as a list of strings.

python
with open("[Link]", "r") as file:
content = [Link]()
print(content)

Writing to a File
To write data, you must open the file in write ( 'w' ) or append ( 'a' ) mode.
 write(string) : Writes a string to the file. You must manually add newline
characters ( \n ).

 writelines(list_of_strings) : Writes a sequence of strings (e.g., a list) to

the file.

python
# Writing (overwrites existing file or creates a new one)
with open("[Link]", "w") as file:
[Link]("Hello, Python!\n")
[Link]("This is a new line.")

# Appending (adds to the end of the file)


with open("[Link]", "a") as file:
[Link]("\nAn appended line.")

Key Considerations
 Closing Files: Explicitly calling [Link]() or using the with statement
is critical for saving changes and freeing system resources.
 Error Handling: Use try...except FileNotFoundError blocks to gracefully
manage scenarios where a file might not exist.

 Text vs. Binary: Python handles text and binary files differently. Text mode
(default) uses encoding (like UTF-8), while binary mode processes raw bytes.

2.7.2 Opening a file using `open()` function


The Python open() function is a built-in function used to open a file and interact
with it for operations like reading or writing. It returns a file object, which is then used
to perform file operations.
Syntax
The basic syntax for the open() function is:
python
open(file, mode)
 file : The name or path of the file you want to open.

 mode : A string specifying the purpose for which the file is opened (e.g., read,

write, append).

Recommended Practice: with open()


It is best practice to use the with statement when handling files. This acts as a
context manager and automatically ensures the file is closed properly, even if errors
occur, which prevents resource leaks.
python
with open("[Link]", "r") as file:
content = [Link]()
print(content)

File Modes
The mode argument is optional and defaults to 'r' (read text mode). Common
modes include:
Mode Description Behavior if File Exists Behavior if File Doesn't Exist

'r' Read (default) Opens for reading. Raises FileNotFoundError

'w' Write Truncates (erases) the file first. Creates a new file.

'a' Append Opens for writing, pointer at the end. Creates a new file.

'x' Exclusive Raises FileExistsError . Creates a new file.


Creation

'b' Binary Used for non-text files (images, etc.).


Combine with other modes,
e.g., 'rb' .

'+' Update Opens for both reading and writing.


Combine with other modes,
e.g., 'r+' .

Example Operations
Reading a file
To read an existing file, use read mode ( 'r' ):
python
try:
with open("my_file.txt", "r") as file:
content = [Link]() # Reads the entire file content
print(content)
except FileNotFoundError:
print("Error: The file was not found.")
Writing to a file
To write new content (overwriting existing content if the file exists), use write mode
( 'w' ):
python
with open("new_file.txt", "w") as file:
[Link]("Hello, World!")
Appending to a file
To add content to the end of a file without deleting existing data, use append mode
( 'a' ):
python
with open("log_file.txt", "a") as file:
[Link]("\nNew log entry.")

2.7.3 Reading from a file (`read()`, `readline()`, `readlines()`)

In Python, read() , readline() , and readlines() are methods used with file
objects to read content, differing primarily in how much data they read and their
return type. It is a best practice to use the with statement to ensure files are
automatically closed.

Method Description Return Type Use Case

read() Reads the entire file (or A single string (or bytes Small to moderate-sized
a specified number of object in binary mode). files where the entire
characters/bytes). content needs to be
processed as one
string.

readline() Reads a single line from A single string. Large files, or when
the file up to the newline processing a file line-by-
character ( \n ). line in a loop to manage
memory efficiently.

readlines() Reads all lines from the A list of strings, with each Files of manageable
file and stores them as string containing a line size where you need to
elements in a list. (including the \ access all lines as a list
n character). for processing.

Examples
Assuming a file named [Link] with the following content:
text
First line.
Second line.
Third line.
Using read()
python
with open("[Link]", "r") as file:
content = [Link]()
print(content)
Output:
First line.
Second line.
Third line.
Using readline()
python
with open("[Link]", "r") as file:
line1 = [Link]()
line2 = [Link]()
print([Link]()) # strip() removes the trailing newline
print([Link]())
Output:
First line.
Second line.
Using readlines()
python
with open("[Link]", "r") as file:
lines_list = [Link]()
print(lines_list)
for line in lines_list:
print([Link]())
Output:
['First line.\n', 'Second line.\n', 'Third line.\n']
First line.
Second line.
Third line.
For iterating through lines, Python provides a more efficient approach that does not
require calling a method explicitly:
python
with open("[Link]", "r") as file:
for line in file:
print([Link]())

2.7.4 Writing to a file (`write()`, `writelines()`)


In Python, data is written to files using the write() and writelines() methods,
after opening the file with the built-in open() function in an appropriate mode (like
'w' for write, 'a' for append, or 'r+' for read/write).
Key Concepts
 File Modes:
o 'w' (Write Mode): Opens a file for writing. If the file exists, it

truncates (empties) the file before writing. If it does not exist, it


creates a new one.
o 'a' (Append Mode): Opens a file for writing. Data is added to the end

of the file without erasing the existing content. If the file does not exist,
it creates a new one.
o 'x' (Exclusive Creation): Creates a new file and opens it for writing.
If the file already exists, it raises a FileExistsError , preventing
accidental overwrites.
o 'b' (Binary Mode): Used with other modes (e.g., 'wb' , 'ab' ) to

handle non-text data like images or executables.


o 'r+' (Read and Write): Opens the file for both reading and writing,

with the file pointer at the beginning. This can lead to overwriting
existing data if not handled carefully.
 Best Practice: Always use the with statement when handling files. This
ensures the file is automatically closed, even if errors occur, preventing data
loss or resource leaks.

The write() Method


The write() method is used to write a single string to a file. It returns the number
of characters written but does not automatically add a newline character ( \n ).
python
with open('[Link]', 'w') as file:
[Link]("Hello, World!\n") # Explicitly add newline character
[Link]("This is a new line of text.")
Contents of [Link] :
Hello, World!
This is a new line of text.
The writelines() Method
The writelines() method writes a sequence (e.g., a list or tuple) of strings to a
file. Like write() , it does not automatically add newline characters, so they must be
included within the strings themselves.
python
lines_list = ["First line\n", "Second line\n", "Third line"]

with open('multiple_lines.txt', 'w') as file:


[Link](lines_list)
Contents of multiple_lines.txt :
First line
Second line
Third line
For efficiency, especially with large lists, you can join the strings into a single large
string and use write() once:
python
lines_list = ["First line", "Second line", "Third line"]

with open('multiple_lines.txt', 'w') as file:


[Link]('\n'.join(lines_list))
Contents of multiple_lines.txt :
First line
Second line
Third line

Summary of Differences
Feature write() writelines()

Argument A single string. An iterable of strings (list, tuple, etc.).


Type

Newlines Must be added manually ( \n ). Must be included within the strings


manually.
Use Case Writing single lines or small pieces of Writing multiple lines of data in bulk
data.

2.7.5 Appending to a file


To append to a file in Python, you open it in append mode using
the open() function with the mode 'a' . It is best practice to use a with statement
to ensure the file is automatically closed.
Using the with statement (Recommended)
The with statement is the recommended approach because it acts as a context
manager, automatically handling file closing even if errors occur.
python
with open('[Link]', 'a') as f:
[Link]('This line will be appended.\\n')
[Link]('And another line.')
 '[Link]' : The path to your file. If the file does not exist, it will be

created.
 'a' : The mode for appending. This ensures new content is added to the end

of the file without overwriting existing data.


 [Link]() : This method adds the string content to the file. You must explicitly
include the newline character \\n if you want each new entry on a separate
line.

Alternative: Manually open() and close()


You can also use the traditional open() and close() methods, but you must
remember to call [Link]() yourself.
python
f = open('[Link]', 'a')
[Link]('Append this text.\\n')
[Link]()
Forgetting to close the file can lead to resource management issues.
Other useful modes
Mode Description

'w' Write mode; overwrites the entire file if it exists, or creates a new one.
'a+' Append and read mode; allows you to append to the end of the file and read its content.
Even if you seek() to the beginning, all writes will still go to the end.

'ab' Append binary mode; used for appending non-text data like images.

2.7.6 Closing a file using `close()`


The close() function is used to terminate the connection between a program and
an open file, which is a critical practice for freeing system resources and ensuring
data integrity.
Purpose and Importance
 Resource Management: Operating systems have limits on the number of
files a single program can have open at once. Calling close() explicitly
deallocates the file descriptor and releases these resources back to the
system.

 Data Persistence: When writing to a file, data is often held in a temporary


memory buffer. The close() method flushes this unwritten information from
the buffer to the actual disk, ensuring all changes are saved permanently.

 Preventing Errors: Operations on a file after it has been closed will raise an
error (like ValueError in Python), which helps in debugging. Not closing files
properly can lead to data corruption or make the file inaccessible to other
processes.

Usage Examples
The syntax for calling close() depends on the programming language, but
generally involves invoking the method on the file object or passing the file
pointer/descriptor to a function.
Language Syntax Example Notes

Python file_object.close() Can be called multiple times without error, but any
operation on the closed file will raise a ValueError .

C fclose(file_pointer); The fclose() function in C returns 0 on success and


EOF on error.
C# [Link](); In C#, using the using statement is the preferred
way, as it automatically calls Dispose() (which
closes the file) when the block ends.

Best Practice: The "With" Statement (Python)


In Python, the most robust way to handle files is by using the with statement (a
context manager), which guarantees the file is closed automatically, even if errors
occur within the block.
python
# Recommended Python approach
with open('[Link]', 'r') as f:
content = [Link]()
# file operations go here
# The file is automatically closed when the 'with' block is exited

2.7.7 Using `with` statement for file handling


The with statement in Python is the recommended and safest way to handle
files because it ensures automatic resource management. It guarantees that the
file is closed properly and immediately after the code block is executed, even if
errors or exceptions occur.
Syntax
The basic syntax for using the with statement for file handling is as follows:
python
with open(file_path, mode) as file_object:
# Perform file operations inside this indented block
content = file_object.read()
# The file is automatically closed when the block ends
 open(file_path, mode) : This function opens the file in the specified mode
(e.g., 'r' for reading, 'w' for writing, 'a' for appending, 'rb' for reading
binary, etc.).
 as file_object : This assigns the opened file to a variable ( file_object in

this case), which is used to interact with the file within the indented block.

Examples
Reading from a file:
python
with open('[Link]', 'r') as file:
content = [Link]()
print(content)
# File is automatically closed here
In this example, the entire content of [Link] is read and printed. When the
code exits the with block, the file is automatically closed.
Writing to a file:
python
with open('[Link]', 'w') as file:
[Link]('Hello, World!\n')
[Link]('This line was written with the with statement.')
# File is automatically closed here
This code opens [Link] in write mode ( 'w' ). If the file doesn't exist, it is
created. If it does, its contents are overwritten. The write() method is used to add
content, and the file is closed automatically when the block finishes.
Nesting with statements for multiple files:
You can manage multiple files simultaneously by nesting with statements or using
a single with statement with multiple context managers.
python
with open('[Link]', 'r') as infile, open('[Link]', 'w') as outfile:
for line in infile:
[Link]([Link]())
# Both files are automatically closed here

Advantages
 Automatic Cleanup: The primary advantage is that you don't have to
explicitly call the close() method, which prevents resource leaks (e.g.,
leaving a file open).
 Exception Safety: Even if an error occurs within the with block, the file will
still be closed. This replaces the need for a verbose try...finally block for
resource management.

 Cleaner Code: It results in more readable and concise code by abstracting


the setup and teardown logic.

2.7.8 Working with text and CSV files (basic intro)


Working with text ( .txt ) and CSV ( .csv ) files involves reading, writing, and manipulating
data, commonly using Python's open() function or csv module. Text files store
unstructured data, while CSV files organize tabular data using commas as delimiters to
separate columns. Key actions include opening files in read ( 'r' ), write ( 'w' ), or append
( 'a' ) modes.
Basic Text File Handling
Text files are human-readable, containing raw text, numbers, or code.

 Reading: file = open('[Link]', 'r') followed


by [Link]() or [Link]() .

 Writing: file = open('[Link]', 'w') followed by [Link]('new


data') .

 Closing: Always use [Link]() to free resources, or use with


open(...) to handle it automatically.

Working with CSV Files


CSV (Comma-Separated Values) files are a specific, widely used format for spreadsheets,
where each line is a data record.

 Reading (Python): Use the csv module to iterate through


rows: [Link](csvfile) .

 Writing (Python): Use [Link](csvfile) and writerow() to create data


with commas.

 Structure: Often include a header row indicating column names.

Common Tools
 Text Editors: Notepad++, TextEdit, or code editors like VS Code are used to view
raw text and check for correct delimiter usage.

 Spreadsheets: Excel and Google Sheets are commonly used to open and edit CSV
files, automatically parsing the columns.

Key Considerations
 Delimiters: While commas are standard, some files use tabs (TSV) or semicolons.

 File Paths: Files can be referenced by absolute paths (e.g., C:\docs\[Link] )


or relative paths based on the current working directory.

 Encoding: Using proper character encoding (like UTF-8) ensures text is read
correctly.
2.8 Handling Exceptions
2.8.1 Understanding exceptions and errors
Exceptions are unexpected, recoverable runtime events (e.g., file not found, invalid input) that
disrupt program flow but can be managed using try-catch blocks to prevent crashes. Errors
are irrecoverable, serious, and typically unhandled system-level problems
(e.g., OutOfMemoryError , StackOverflowError ) that terminate applications.

Key Differences at a Glance


Feature Exception Error

Nature Unexpected event, recoverable Serious, unrecoverable

Handlin Can be caught ( try-catch ) Cannot be caught/handled


g

Timing Runtime Runtime (or compile-time)

Cause Application logic issues System/Environment failure

Exampl FileNotFoundException , DivideByZe OutOfMemoryError , StackOverfl


e ro ow

Understanding Exceptions
Exceptions occur during execution when something unexpected happens. They are
meant to be handled by the programmer to ensure the application continues
running.
 Checked Exceptions: Detected at compile-time (e.g., IOException ).

 Unchecked Exceptions: Detected at runtime (e.g., ArithmeticException ).

Understanding Errors
Errors indicate problems that the application should not attempt to catch, as they
often signify a failure in the environment or resources.
 System Crashes: Such as JVM running out of memory.

 Irrecoverable: The program cannot recover from these.

Best Practices for Handling


 Use Exceptions for Expected Failures: Handle cases like missing files or
invalid network responses.
 Avoid Catching Errors: Allow errors to terminate the program, as they
cannot be resolved gracefully.

 Log Exception Details: Use stack traces to diagnose and fix exceptions.

2.8.2 Try and except block


The try...except block in Python is used for exception handling, allowing you to
manage runtime errors gracefully and prevent your program from crashing. It
provides a way to separate the potentially error-prone code from the code that
handles the errors.
Basic Syntax and Function
The basic structure involves a try block and one or more except blocks:
python
try:
# Code that might raise an exception
result = 10 / 0
except ZeroDivisionError:
# Code to handle the specific exception if it occurs
print("Error: Cannot divide by zero!")
 try block: Contains the code that may raise an exception during execution.

 except block: Executes if a specified exception is raised within


the try block. The program then continues running after the
entire try...except statement, instead of stopping abruptly.

Handling Specific Exceptions


It is best practice to catch specific exceptions rather than using a
generic except statement, as this helps prevent masking unexpected errors.
python
try:
number = int(input("Enter a number: "))
result = 10 / number
except ValueError:
print("Invalid input! Please enter a numeric value.")
except ZeroDivisionError:
print("You cannot divide by zero!")
You can also handle multiple exceptions with a single except block by grouping
them in a tuple:
python
try:
# ... risky code ...
except (ValueError, TypeError) as e:
print(f"An exception occurred: {e}")
Additional Clauses: else and finally
The try statement can also include optional else and finally blocks for more
control:
 else block: The code in the else block runs only if the code in
the try block executes successfully without raising any exceptions. This is
useful for code that should only run on success.
 finally block: The code in the finally block always executes, regardless

of whether an exception occurred or not. This is typically used for essential


cleanup actions, such as closing files or network connections, to ensure
resources are released even if an error occurs.

Here is an example using all the clauses:


python
try:
file = open("[Link]", "r")
content = [Link]()
except FileNotFoundError:
print("File not found!")
else:
print("File content successfully read!")
finally:
[Link]() # The file is closed in all scenarios
print("Resource handling complete.")

2.8.3 Catching specific exceptions (`ZeroDivisionError`, `ValueError`,


etc.)
In Python, you catch specific exceptions using multiple except clauses in
a try...except block. This practice provides meaningful error messages, handles
different errors appropriately, and avoids masking unexpected bugs.
Syntax for Catching Specific Exceptions
To catch different exceptions, use a separate except clause for each potential error
type:
python
try:
# Code that might raise an exception
user_input = int(input("Enter a number: "))
result = 10 / user_input
except ValueError:
# Handler for invalid input type (e.g., user enters a string)
print("Error: Invalid input. Please enter a valid integer.")
except ZeroDivisionError:
# Handler for division by zero (e.g., user enters 0)
print("Error: Cannot divide by zero!")
except Exception as e:
# A general handler for any other unexpected exceptions (optional, but
specific handlers are preferred)
print(f"An unexpected error occurred: {e}")
else:
# Code to run if no exception occurred in the try block
print(f"Result: {result}")
finally:
# Code that runs regardless of whether an exception occurred (optional,
often for cleanup)
print("Execution complete.")

Key Points
 Specificity Matters: It is best practice to catch specific exceptions rather than
using a generic except statement. Catching a broad Exception (or a
bare except ) can hide bugs and make debugging difficult.

 Multiple Exceptions in One Block: If the same action is needed for multiple
exceptions, you can catch them in a single except clause using a tuple:

python
try:
# Some risky code
pass
except (ValueError, TypeError, ZeroDivisionError):
print("An input or calculation error occurred.")
 Execution Flow: Only one except block will execute. If an exception is
raised, Python looks for the first except clause that matches the exception
type (or a parent class of the exception).
 Error Information: You can capture the specific error message using
the as keyword (e.g., except ValueError as e: ) to access details about
the error that occurred.

2.8.4 The `else` and `finally` blocks


In Python, else and finally are optional blocks in exception handling that
enhance flow control and resource management. The else block runs only
if no exceptions are raised in the try block, whereas the finally block
executes always, regardless of whether an error occurred or was caught, making it
ideal for cleanup actions.
Key Differences and Usage:
 else Block:

o Purpose: Executes code that should only run if the try block
succeeds.

o Behavior: Runs only if no exceptions occur.

o Benefit: Keeps the try block focused on code that might raise
exceptions, improving readability.
 finally Block:

o Purpose: Runs cleanup code (e.g., closing files, releasing database


connections).

o Behavior: Executes regardless of whether an exception is raised,


caught, or if a return , break , or continue statement is used.

o Benefit: Ensures critical resources are released even if unexpected


errors occur.

Execution Flow:
1. try : Runs code that may raise an exception.

2. except : Runs if an exception occurs.

3. else : Runs if no exception occurs (after try , before finally ).

4. finally : Runs always as the last step.

Example:
python
try:
f = open("[Link]", "r")
content = [Link]()
except IOError:
print("File not found.")
else:
print("File read successfully.") # Runs if no exception
finally:
print("Closing file.")
[Link]() # Always runs, even if file read fails
Common Pitfalls:
 Placing code in else that should be protected by except .

 Using finally to re-raise exceptions, which can obscure the original error.

2.8.5 Raising exceptions using `raise`


The raise statement in Python is used to manually trigger, or "throw", an exception
when an error or unexpected condition occurs in a program's execution. This allows
the programmer to enforce specific conditions and handle errors in a controlled
manner, preventing the program from terminating abruptly with a default error
message.
Syntax
The basic syntax for the raise statement is:
python
raise ExceptionClass("Optional error message")
 ExceptionClass : The type of built-in
(e.g., ValueError , TypeError , ZeroDivisionError ) or custom exception
you want to raise.
 "Optional error message" : A string argument that provides additional

information about the error, which is helpful for debugging.

Examples
1. Raising a built-in exception with a message:
This is useful for input validation, ensuring arguments to a function meet certain
criteria.
python
def check_age(age):
if age < 0:
raise ValueError("Age cannot be negative.")
if age >= 18:
return "You are an adult."
else:
return "You are a minor."

try:
print(check_age(-5))
except ValueError as e:
print(f"An error occurred: {e}")
Output:
An error occurred: Age cannot be negative.
2. Re-raising an exception:
A bare raise statement within an except block re-raises the exception that was
most recently active in the current scope. This is useful if you want to log an error or
perform cleanup actions before allowing the exception to propagate up the call
stack.
python
import logging

try:
result = 10 / 0
except ZeroDivisionError as e:
[Link]("A division by zero error occurred")
raise # Re-raises the ZeroDivisionError
3. Chaining exceptions (using from ):
You can catch one exception and raise a new, different exception while preserving
the original traceback information using the from clause. This helps in providing
more context for complex debugging.
python
try:
# Some operation that might fail
raise ConnectionError("Network connection failed")
except ConnectionError as exc:
raise RuntimeError("Failed to connect to the external service") from
exc
Output would show tracebacks for both ConnectionError and RuntimeError ,
indicating that the first caused the second.

2.8.6 Best practices for exception handling


Best practices for exception handling include catching specific exceptions rather than
generic ones, using finally blocks for resource cleanup, and avoiding empty catch
blocks to ensure errors are logged and handled appropriately. Focus on failing fast,
using descriptive error messages, and maintaining clean, readable code by
keeping try blocks focused.
Key best practices for robust exception handling:
 Catch Specific Exceptions: Avoid catching Exception or
generic Throwable classes, which can mask bugs.

 Use finally for Cleanup: Always close resources (files, database


connections) in a finally block or use context managers (like
Python's with statement) to ensure they close even if an exception occurs.

 Never Ignore Exceptions: Empty catch or except blocks (swallowing


exceptions) make debugging nearly impossible. At minimum, log the
exception.

 Log Exception Details: Log the full stack trace and relevant context to aid in
debugging.

 "Throw Early, Handle Late": Detect errors as soon as possible and


propagate them to a higher-level handler that knows how to properly respond
to the error.

 Don't Log and Rethrow: Avoid logging an exception and then rethrowing it,
as this results in multiple log entries for the same error.

 Use Custom Exceptions: Create specific exception subclasses to provide


clearer, more descriptive error information.
 Use try-else (Python): Use the else clause to execute code that should
only run if the try block succeeds, which keeps the try block focused only
on code that might throw an error.
 Keep try Blocks Small: Minimize the amount of code in a try block to
make it clear which operation failed.

python
# Example of best practices
try:
# Focused, specific action
with open('[Link]', 'r') as f:
data = [Link]()
except FileNotFoundError as e:
# Handling specific exception
[Link](f"File not found: {e}")
except IOError as e:
# Handling specific exception
[Link](f"Error reading file: {e}")
else:
# Only runs if try succeeds
process(data)
finally:
# Guaranteed cleanup
close_resources()
 Avoid Using Exceptions for Control Flow: Exceptions should handle
unexpected situations, not normal program logic.
 Don't Catch Throwable (Java): It is a best practice to avoid
catching Throwable , as it covers errors you shouldn't try to handle.

 Validate Input: Use input validation to prevent preventable exceptions.

2.9 Using Python Libraries


2.9.1 What is a Python library/module?
A Python module is a single file containing Python code (definitions and statements) that
can be imported and used in other Python programs. A Python library is a broader, informal
term for a collection of related modules and packages that provide reusable functionality to
avoid writing code from scratch.

Python Module
A module is the fundamental unit of code organization in Python.

 Definition: A module is simply a Python file with a .py extension.

 Content: It can define functions, classes, and variables, or contain runnable code.

 Usage: You use the import statement to access the module's contents in another
file.

 Benefit: Modules help in breaking down large programs into manageable, logical,
and reusable pieces of code, which improves organization and maintainability.
Example:
If you have a file named [Link] with an add() function, you can use it in another script
like this:
python
# [Link]
def add(x, y):
return x + y
python
# main_script.py
import calc
result = [Link](10, 20)
print(result) # Output: 30

Python Library
The term "library" is less formal in Python than "module" or "package" but is commonly used
to refer to a substantial body of code designed for a specific domain or set of tasks.

 Definition: A library is generally a collection of related modules (and possibly


packages, which are directories of modules) that together offer a wide range of
functionalities.

 Usage: Libraries are imported to perform common, complex tasks efficiently without
having to write the underlying logic yourself.

 Types:

o Standard Library: A vast collection of modules that comes bundled with


every Python installation (e.g., math , os , datetime ).

o External/Third-Party Libraries: These are installed separately using a


package manager like pip (e.g., NumPy for numerical
computing, Pandas for data analysis, and Requests for HTTP requests)

2.9.2 Importing built-in libraries using `import`, `from ... import`


In Python, you can import built-in libraries using the import statement or
the from ... import statement. Built-in modules like math , os ,
and datetime are readily available for use without any prior installation.
Using import
The import statement brings the entire module into your program. You must then
use the module name as a prefix to access its functions, classes, or variables.
Syntax: import <module_name>
Example (using the math module):
python
import math

# Access the pi constant and the sqrt function using the 'math.' prefix
print([Link])
print([Link](16))
Output:
3.141592653589793
4.0
You can also create an alias for the module using the as keyword to make the
name shorter or clearer.
Example (with alias):
python
import math as m

# Use the alias 'm.' as a prefix


print([Link])
print([Link](25))
Using from ... import
The from ... import statement allows you to import specific components
(functions, variables, etc.) directly into your program's namespace. This means you
can use those components without the module name prefix.
Syntax: from <module_name> import <component_name1>, <component_name2>
Example (importing specific items from math ):
python
from math import pi, sqrt

# Use pi and sqrt directly


print(pi)
print(sqrt(36))
Output:
3.141592653589793
6.0
You can also import multiple items and give them aliases.
Example (with aliases):
python
from math import pi as my_pi, sqrt as square_root

print(my_pi)
print(square_root(49))
Using from ... import * (Wildcard Import)
This imports all functions and variables from a module into your current namespace.
While convenient, it is generally discouraged in professional code as it can lead to
namespace pollution and potential name conflicts with existing variables or
functions.
Syntax: from <module_name> import *
Example:
python
from math import *

# pi and sqrt can be used directly, but this practice is generally not
recommended
print(pi)
print(sqrt(64))

2.9.3 Overview of standard libraries useful in Data Science


Essential Python libraries for data science include Pandas and NumPy for data
manipulation and numerical computation, Matplotlib and Seaborn for visualization,
and Scikit-learn for machine learning. TensorFlow and PyTorch are crucial for
deep learning, while SciPy and Statsmodels are used for scientific computing and
statistical analysis.
Key Data Science Libraries:
 Data Manipulation & Analysis:

o Pandas: The core library for structured data manipulation, offering


DataFrames for cleaning and analyzing data.
o NumPy: Fundamental package for high-performance numerical
computing and multi-dimensional arrays.

o Polars: A fast, modern alternative to Pandas for large datasets.

 Data Visualization:

o Matplotlib: A foundational library for creating static, interactive, and


animated plots.

o Seaborn: A high-level library based on Matplotlib for attractive


statistical graphics.

o Plotly: Used for creating interactive, web-based, and dynamic


visualizations.

 Machine Learning & Modeling:

o Scikit-learn (sklearn): A comprehensive, user-friendly library for


traditional machine learning tasks, including classification, regression,
and clustering.

o XGBoost/LightGBM: Popular, high-performance libraries for gradient


boosting, frequently used for tabular data.

o Statsmodels: Used for estimating, performing, and interpreting


complex statistical models.

 Deep Learning:

o TensorFlow/Keras: A popular framework for designing and training


deep neural networks.

o PyTorch: A widely used, flexible, and efficient framework for deep


learning and neural networks.

 Specialized Libraries:

o SciPy: Extends NumPy for technical computing, including optimization


and signal processing.

o NLTK/spaCy: Key libraries for Natural Language Processing (NLP)


tasks.

o Beautiful Soup/Scrapy: Essential for web scraping and data


extraction.
o Requests: Used for handling HTTP requests to interact with web
APIs.

2.9.4 Installing and importing third-party libraries (e.g., `pandas`,


`numpy`)
Installing and importing third-party Python libraries is a two-step process involving
a package manager for installation and the import statement for use in your code. The
most common package manager is pip.

1. Installing Libraries with pip

The pip tool is the standard package manager for Python and is usually included with
modern Python installations. It downloads and installs packages from the Python Package
Index (PyPI).

To install a specific library like pandas or numpy , open your computer's terminal or
command prompt (not the Python interactive shell) and run the following command:

bash
pip install package_name
For the specific examples in your request, you would run:

bash
pip install pandas numpy
Best Practices for Installation

 Use a Virtual Environment: It is highly recommended to use a virtual environment


to isolate project-specific dependencies and avoid conflicts with other projects or your
system's Python installation. You can create one using venv :

bash
python -m venv my_project_env
# Activate the environment (command varies by OS)
# On Windows: .\my_project_env\Scripts\activate
# On macOS/Linux: source my_project_env/bin/activate
Once activated, any pip install command will install packages into that specific
environment.

 Alternative: Anaconda/Miniforge: For data science, a distribution


like Anaconda or Miniforge is popular as it comes pre-bundled
with pandas , numpy , and many other scientific libraries, managed by
the conda package manager.

2. Importing Libraries in Python


Once installed, you can use the libraries in your Python script or notebook using
the import statement. There are several ways to import:

 Import the entire library: The most common way. You'll then need to use the library
name (or alias) as a prefix for its functions.

python
import pandas as pd
import numpy as np
(Using pd and np as aliases is a widely accepted convention for conciseness).

 Import the entire library without an alias:

python
import numpy
# Usage: [Link](...)
 Import specific functions/classes:

python
from pandas import DataFrame, read_csv
# Usage: DataFrame(...)

Example Usage
After installing and importing, you can start using the library's functionality:

python
import pandas as pd
import numpy as np

# Create a sample NumPy array


data_array = [Link]([1, 2, 3, 4, 5])
print("Numpy array:", data_array)

# Create a Pandas DataFrame from the NumPy array


df = [Link](data_array, columns=['Numbers'])
print("Pandas DataFrame:")
print([Link]())

2.9.5 Exploring module documentation with `help()` and `dir()`


The help() and dir() functions in Python are built-in utilities primarily used for
introspection and exploration within a Python environment.
dir() function
The dir() function is used to find out which names are defined inside a module,
class, or object, or within the current scope. It returns a list of attributes and methods
of the specified object.
 Usage: It helps you discover what an object can do.

 Syntax: dir([object])

 Key behaviors:
o If no argument is passed, dir() lists the names in the current local
scope.

o If given a module, class, or class instance, it attempts to return a list of


valid attributes and methods for that object.

o The list typically includes both user-defined names and built-in names
(starting with __ , like __init__ , __str__ ).

Example:
python
>>> dir(list)
['__add__', '__class__', ..., 'append', 'clear', 'copy', 'count', 'extend',
'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

>>> my_list = [1, 2]


>>> dir(my_list)
# Returns a similar list of methods available on that list instance.

help() function
The help() function is part of Python's built-in documentation system. It retrieves
and displays the documentation string (docstring) associated with a given object.
 Usage: It is used to understand how a function, module, class, or keyword
works without leaving the interpreter.
 Syntax: help([object])
 Key behaviors:
o If an argument is passed, help() displays the help documentation for
that object.

o If no argument is passed, it starts an interactive help utility in your


console session, where you can type names of functions, modules, or
keywords to get documentation.

Example:
python
>>> help(print)
Help on built-in function print in module builtins:

print(...)
print(value, ..., sep=' ', end='\n', file=[Link], flush=False)

Prints the values to a stream, or to [Link] by default.


...

>>> help([Link])
Help on method append in module builtins:

append(object, /) method of [Link] instance


Append object to the end of the list.

2.9.6 Difference between module, package, and library


In Python, a module is a single file of code, a package is a directory containing
related modules and sub-packages, and a library is a generic, high-level term for a
collection of modules and packages that offer reusable functionality.
Module
 Definition: A module is a single Python file with a .py extension containing
functions, classes, and variables. It is the smallest organizational unit of
Python code.

 Purpose: To organize and reuse code within a single application or across


multiple applications by providing a namespace. For example, the built-
in math module provides mathematical functions.
 Physical Form: A single .py file.

Package
 Definition: A package is a collection of related modules organized in a
directory hierarchy, which may also contain sub-packages. It helps structure a
large codebase by logically grouping related modules.

 Purpose: To provide a hierarchical naming structure using "dotted module


names" (e.g., import [Link] ) and manage namespaces
effectively, preventing naming conflicts.

 Physical Form: A directory containing Python files (modules) and an


optional __init__.py file (required in Python 2, but optional in Python 3.3+
for namespace packages).

Library
 Definition: The term "library" is a more general, conceptual term, not a strictly
defined Python technical construct like a module or a package.

 Purpose: It generally refers to a large collection of code (which can be one or


more packages or modules) designed to be used by many different
applications to perform specific tasks, such as scientific computing ( NumPy ) or
data visualization ( Matplotlib ).

 Physical Form: Can be a single module or, more commonly, a collection of


packages, often distributed via the Python Package Index (PyPI) and installed
using tools like pip .

You might also like