0% found this document useful (0 votes)
3 views

Unit_1_2_Python

The document provides an introduction to Python programming, covering basic elements such as objects, variables, and functions. It explains the differences between programming languages, the advantages of Python, and its limitations, while also detailing how the book will teach Python concepts. Key topics include variable assignment, expressions, and the use of Python's shell for executing commands.

Uploaded by

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

Unit_1_2_Python

The document provides an introduction to Python programming, covering basic elements such as objects, variables, and functions. It explains the differences between programming languages, the advantages of Python, and its limitations, while also detailing how the book will teach Python concepts. Key topics include variable assignment, expressions, and the use of Python's shell for executing commands.

Uploaded by

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

Unit-1 Introduction to Python

Unit-2 Functions, Exception, Modules and Files

Unit-1 Introduction to Python


The basic elements of Python, Objects, expressions and numerical Types,
Dictionaries Variables and assignments, IDLE, Branching programs, Strings and
Input, Iteration.
Structured Types, Mutability and Higher-order Functions: Tuples, Ranges,
Lists and Mutability (Cloning and list comprehension), Strings, Tuples and Lists

2. Introduction to Python

🔹 Understanding Programming Languages


Every programming language is different in how it looks and works, but they can be grouped and compared
using a few key ideas:

1. Low-level vs High-level Languages

 Low-level language:
o Closer to machine language (what the computer understands directly).
o Involves very specific tasks like moving data bit by bit.
o Example: Assembly Language.
 High-level language:
o Easier for humans to read and write.
o Uses abstract operations like "print on screen" or "show menu".
o Example: Python, Java, C++.

📝 High-level languages help us focus on solving problems instead of worrying about how the computer
works inside.

2. General-purpose vs Domain-specific Languages

 General-purpose:
o Can be used for building all kinds of software (games, apps, websites, etc.).
o Example: Python – you can build websites, games, software tools, and more.
 Domain-specific:
o Built for one specific task or field.
o Example: Adobe Flash – designed mainly for animations and interactive web elements.
o Not suitable for tasks like financial analysis or data processing.
📝 Python is general-purpose. That’s why it’s so widely used.

3. Interpreted vs Compiled Languages

 Source Code: The code you write as a programmer.


 Interpreted Language:
o Code is run line-by-line using an interpreter.
o Easy to catch and fix errors.
o Example: Python.
 Compiled Language:
o The whole code is converted into machine code first, then executed.
o Runs faster, but harder to debug.
o Example: C, C++.

📝 Python is interpreted, which is why it’s great for beginners – error messages are helpful and easier to
understand.

🐍 Why Python Is Used in This Book


 The book uses Python to teach problem-solving through programming.
 It’s not a complete guide to Python, but it helps you learn how to write code to solve real-world
problems.
 Once you learn these concepts, you can use them in any language.

🔎 What is Python?
 A general-purpose programming language.
 Easy to read and write.
 You can use it to build almost anything (except things that need very close control of the computer
hardware).

🔺 Limitations of Python:

 Not ideal for:


o Programs that require very high reliability (like flight control systems).
o Projects handled by large teams or long-term development – because it does not have strong
error-checking before running (weak static semantic checking).

📝 In simple words: Python is great for learning and building apps, but not the best for highly sensitive or
long-term projects.

✅ Advantages of Python
1. Simple and easy to learn – Good for beginners.
2. Interactive and interpreted – Helpful for understanding what your code is doing.
3. Huge community and tons of free libraries:
o You don’t need to build everything from scratch.
o Examples: NumPy (math), Flask (web), Pygame (games), etc.

📘 How This Book Teaches Python


 Python is introduced step by step – only what is needed to explain a concept.
 Not every feature of Python is explained.
 For anything more, free online resources are suggested (like Python official docs, W3Schools, etc.).

📝 This book helps you focus on thinking like a programmer, not memorizing Python rules.

📜 Brief History of Python


 Created by Guido van Rossum in 1990.
 First used by few people only.

🔹 Major Versions

 Python 2.0 (2000):


o Made the language better and attracted more developers.
 Python 3.0 (2008):
o Removed old, confusing features.
o Cleaner and more modern.
o But it was not backward compatible (old Python 2 programs won’t run directly on Python
3).

📝 This means older Python code may not work in newer versions.

💻 Why the Book Uses Python 2.7


 At the time of writing, some important libraries still didn’t work with Python 3.
 Python 2.7 includes many good features from Python 3.
 So, the book uses Python 2.7 for compatibility and practical use.

📌 Final Thoughts
 Python is just a tool to teach you how to think and solve problems using programming.
 Once you learn that, switching to any other language is much easier.
2.1. The Basic Elements of Python (Simplified Notes)

🐍 What is a Python Program?

 A Python program (also called a script) is a set of instructions that you write to get the computer to do
something.
 These instructions can include:
o Definitions (like defining variables or functions)
o Commands (also called statements) that tell Python what to do

⚙️ What is a Python Shell?

 The Python interpreter is a program that runs your Python code.


 When you run a Python program, it usually opens a Python shell.
 A shell is a window where your code is evaluated and executed.

✅ Tip: You can try code directly in the Python shell. It's great for practice and learning.

📢 What is a Command or Statement?

 A command (also known as a statement) is a line of code that tells Python to perform an action.
 For example:

print 'Yankees rule!'

This tells Python to print the message 'Yankees rule!' to the screen.

📝 In Python 2, the print statement doesn't require parentheses. In Python 3, you must use parentheses:
print('Yankees rule!')

💡 Example of Multiple Commands:

Here’s a sequence of commands:

print 'Yankees rule!'


print 'But not in Boston!'
print 'Yankees rule,', 'but not in Boston!'

✅ Output:
Yankees rule!
But not in Boston!
Yankees rule, but not in Boston!
🎯 What Happened?

 The first two lines printed two separate lines of text.


 The third line gave two values to the print command.

print 'Yankees rule,', 'but not in Boston!'

 It printed both, separated by a space.


 That’s how Python’s print works: when you give it multiple items, it prints them with a space in between.

🔎 Summary of Key Points

Concept Description

Python Program A series of definitions and commands.

Python Shell Where Python code runs and gives output.

Command Also called a statement; tells Python to perform a specific task.

print A command used to display text or values on the screen.

Multiple values If passed to print, they're printed in the given order with spaces.

2.1.1. Objects, Expressions, and Numerical Types

🔸 What is an Object in Python?

 In Python, everything is an object — numbers, text, even functions!


 An object is something Python can manipulate, store, or return.
 Every object has a type, which defines:
o What kind of object it is (like number, string, etc.)
o What operations you can perform on it

🔹 Types of Objects: Scalar vs Non-Scalar

Type Description Example

Scalar Indivisible, no internal structure Numbers, Booleans

Non-Scalar Have internal structure, can be broken down Strings, Lists

Think of scalar as atoms and non-scalar as molecules.

🔸 4 Scalar Types in Python


Type Description

int Integer values (whole numbers) → -3, 5, 10002

float Real numbers with decimals → 3.0, -28.72, 1.6E3 (=1600.0)

bool Boolean values → True, False

None Represents “nothing” or “no value” → used in functions, variables

🧠 What is an Expression?

 An expression is a combination of objects and operators that Python evaluates to produce a value.

💬 Examples:
3 + 2 # Result: 5 → type int
3.0 + 2.0 # Result: 5.0 → type float
3 != 2 # Result: True → type bool

🔍 Using the type() Function

 Python’s built-in type() function tells you the type of any object.

type(3) # Output: <class 'int'>


type(3.0) # Output: <class 'float'>

➕ Arithmetic Operators in Python

Operator Meaning Example Result

+ Addition 2 + 3 5

- Subtraction 5 - 2 3

* Multiplication 3 * 4 12

** Exponent (Power) 2 ** 3 8

// Integer Division 6 // 4 1

/ True Division (float) 6 / 4 1.5

% Modulus (Remainder) 7 % 4 3

⚠️ In Python 3, / always gives a float result, even with integers!

🔁 Operator Precedence
Python follows a specific order when evaluating expressions.

*, /, //, % → are done before +, -


Use parentheses to change the default order.

x + y * 2 # Multiply first, then add


(x + y) * 2 # Add first, then multiply

🔍 Comparison Operators

Operator Meaning Example Result

== Equal 3 == 3 True

!= Not equal 3 != 2 True

> Greater than 5 > 3 True

< Less than 2 < 4 True

>= Greater or equal 3 >= 3 True

<= Less or equal 2 <= 1 False

🔘 Boolean (Logical) Operators

Operator Description Example Result

and True if both conditions are True True and False False

or True if at least one is True True or False True

not Reverses the truth value not True False

🐚 The Python Shell Prompt >>>

When you see:

>>> 3 + 2

This means Python is waiting for your input in the shell.


The line below shows the result/output.

🎓 Quick Recap
 Python manipulates objects, and every object has a type.
 Scalar types: int, float, bool, and None.
 Expressions combine values and operators to produce new values.
 Use type() to check an object’s type.
 Know the basic operators and how to use comparison and boolean logic.

2.1.2. Variables and Assignment


What is a Variable?

 Variables in Python (and programming in general) are like labels for data. They are used to store values that
you can use later in your program. Instead of writing a number or a string repeatedly, you can assign it to a
variable and then refer to that variable.
 Example:

pi = 3
radius = 11
area = pi * (radius ** 2)

In this code:

o pi is a variable that holds the value 3.


o radius is a variable that holds the value 11.
o area is a variable that stores the result of the formula for the area of a circle, which is pi *
(radius ** 2).

How Does Assignment Work?

 Assignment means giving a variable a value. In the example above:


o pi = 3 means that pi is now bound to the value 3.
o radius = 11 means that radius is bound to 11.
o area = pi * (radius ** 2) calculates the area using the value of pi (3) and radius (11),
which results in the area.

The key here is that variables store objects. An object can be a number, a string, or even a more complex
data type like a list or a function.

Visualizing it: When we assign values to variables, we can think of variables as boxes that store values.
Here's a quick illustration:

o Initially:

pi -> 3
radius -> 11

o After calculating the area:

area -> 3 * (11 ** 2) = 3 * 121 = 363


Rebinding Variables

 You can rebind a variable to a new value later in the program. For example, changing the value of radius:

radius = 14
This doesn’t affect the value of area because area is still bound to the result of pi * (11 ** 2), not to
the new value of radius. Essentially, area remembers its original value.

A Variable is Just a Name

 In Python, a variable is just a name. It doesn’t "hold" or "contain" a value itself; rather, it is linked or
associated with the value. That’s why Python allows multiple variables to refer to the same value. You can
have many variables pointing to the same object, or even no variables pointing to an object.

Important: The value (or object) can exist in memory without having any variable pointing to it, or it can be
referred to by many variables.

Good Naming of Variables

Why Does Naming Matter?

 In Python, variables are just names for objects, but good naming of variables plays a crucial role in making
the code understandable for humans.
 Well-chosen variable names help programmers (and your future self) understand the code easily and
quickly.

Example:

a = 3.14159
pi = 3.14159
b = 11.2
diameter = 11.2
c = a * (b ** 2)
area = pi * (diameter ** 2)

o The code above calculates the area of a circle, but the second version uses more meaningful
variable names like pi and diameter. This makes it clearer what the variables represent, even
though both versions do the same thing.
o The first example uses a and b, which could make it hard to understand what the code is doing if
someone unfamiliar with it reads it.
Python Variable Rules

 Variable names in Python must follow certain rules:


o Letters (a-z, A-Z), digits (0-9), and underscores (_) are allowed.
o Cannot start with a digit (e.g., 1radius is invalid).
o Case-sensitive: radius, Radius, and RADIUS are different variables.
 Reserved words (keywords): These are words that Python uses for special purposes, like if, for, while,
etc. You cannot use these as variable names because they have specific meanings in Python.

Examples of Reserved Words:

o and, or, not, if, else, while, for, class, try, except, etc.

List of Reserved Words (in Python 2.7):

and, as, assert, break, class, continue, def, del, elif, else, except, exec,
finally, for, from,global, if, import, in, is, lambda, not, or, pass, print,
raise, return, try, with, while, yield

Comments in Python

 Comments are lines in the code that are ignored by Python. They are useful for adding explanations or notes
for humans reading the code.

A comment starts with # and continues to the end of the line.

Example:

# This is a comment. Python ignores it.


areaC = pi * radius ** 2 # Calculate the area of the circle

o Anything after # is not processed by Python and is only there for the programmer.

Multiple Assignment in Python

 Python allows you to assign values to multiple variables in a single line. This is called multiple assignment.

Example:

x, y = 2, 3

This assigns 2 to x and 3 to y in one line.

 Python evaluates all expressions on the right-hand side before any of the assignments happen.
 Swapping values: You can also swap the values of two variables using multiple assignment:

Example:

x, y = 2, 3
x, y = y, x # Swap the values
print('x =', x) # x = 3
print('y =', y) # y = 2
In this case, x becomes 3 and y becomes 2.

Summary

 Variables are names used to refer to values.


 Assigning a value to a variable uses the = sign.
 Good variable names help make the code readable and understandable.
 Python has rules for valid variable names and a list of reserved words.
 You can add comments to your code for better understanding.
 Multiple assignment allows you to assign values to multiple variables in one line and is useful for tasks like
swapping variable values.

2.1.3 IDLE
What is IDLE?

 IDLE is an integrated development environment (IDE) that comes pre-installed with Python.
 It provides a user-friendly interface for writing, editing, and running Python programs, making it easier to
work with Python.

Why is IDLE Used?

 Typing Python programs directly into the shell (a command-line interface) is not convenient for larger
programs. An IDE like IDLE makes it much easier to write and manage Python code because it provides
various useful features like syntax highlighting, autocompletion, and smart indentation.
 IDLE helps streamline the process of developing Python programs by combining a text editor and a Python
shell in one environment.

Features of IDLE

1. Text Editor:
o IDLE provides a built-in text editor that helps you write Python programs.
o The editor includes features like:
 Syntax Highlighting: It highlights different parts of the code in different colors to make it
easier to read (e.g., keywords, variables, and functions).
 Autocompletion: This feature suggests completions for code as you type, helping you to
write code faster and with fewer errors.
 Smart Indentation: It automatically aligns your code properly to keep it neat and readable.
2. Shell:
o When you open IDLE, it also opens a Python shell window. This shell allows you to interactively type
and test Python commands one by one.
o The shell also has syntax highlighting to make your code easier to understand.
o You can execute single lines or small blocks of code in the shell to quickly test things.
3. Integrated Debugger:
o IDLE includes a debugger, which is a tool used to find and fix errors in your code. However, for now,
you can ignore the debugger and focus on writing and running your code.
How to Use IDLE

 Starting IDLE:
o IDLE is just like any other application on your computer. You can start it by double-clicking on the
IDLE icon or by searching for "IDLE" in your computer’s start menu or applications list.
 Main Features of the IDLE Interface:
o Shell Window: This is where you can type individual Python commands and see their output
immediately.
o File Menu: This menu allows you to work with Python files.
 Create a new file: To start a new Python program, click on "File" → "New File", which opens
a new window where you can write your Python code.
 Open an existing file: To edit an existing Python program, use "File" → "Open" to load a .py
file.
 Save the current file: To save your work, click on "File" → "Save" and choose a location to
store your file (files are saved with the .py extension).
o Edit Menu: This menu provides typical text-editing commands like copy, paste, and find. It also
includes Python-specific commands:
 Indent Region: Automatically indents selected lines of code.
 Comment Out Region: Adds a # symbol before each line in the selected region, which makes
those lines comments (ignored by Python).

Conclusion

 IDLE is a simple and powerful tool for writing Python programs. It provides a text editor and a shell for
writing and testing Python code.
 It is beginner-friendly, with features like syntax highlighting, autocompletion, and smart indentation,
making coding in Python easier and more efficient.

2.2. Branching Programs


Branching programs are an extension of straight-line programs and introduce the concept of conditional statements.
These allow a program to make decisions and follow different paths depending on certain conditions.

What Are Branching Programs?

 Straight-line programs are simple and execute one statement after another, without making any decisions.
They are quite limited because they can only execute a fixed sequence of statements.
 Branching programs, on the other hand, include conditional statements that allow the program to decide
which block of code to execute based on certain conditions. This makes them more flexible and interesting,
as the program can take different paths depending on inputs or other factors.

Conditional Statements

 The simplest branching statement is a conditional statement (also known as an if-else statement).

A conditional has three parts:

1. Test (Boolean expression): An expression that evaluates to True or False.


2. Block of code for True: A block of code that runs if the test evaluates to True.
3. Block of code for False: An optional block of code that runs if the test evaluates to False.

The general syntax for an if-else statement in Python is:

if Boolean_expression:
# Block of code if condition is True
else:
# Block of code if condition is False

Example: Even or Odd Program

Here's a simple program that checks if a number is even or odd:

x = 10 # You can change this value for testing

if x % 2 == 0:
print('Even')
else:
print('Odd')

print('Done with conditional')

 Explanation:
o x % 2 == 0 checks if x is divisible by 2 (i.e., if it is even).
o If the condition is True, the program prints "Even". If it's False, it prints "Odd".
o The print('Done with conditional') line always runs after the conditional is evaluated.
 Note on Indentation:
o In Python, indentation is crucial. It defines the blocks of code that belong to the if or else
statements.
o If you mistakenly change the indentation level, Python will raise an IndentationError.
o Python uses indentation instead of braces {} (like in C or Java), making the code visually organized
and easy to understand.

Nested Conditionals

You can also have nested conditionals, where one if statement appears inside another if or else block. This
allows you to check multiple conditions in a structured way.

Example:

x = 6 # You can change this value for testing


if x % 2 == 0:
if x % 3 == 0:
print('Divisible by 2 and 3')
else:
print('Divisible by 2 but not by 3')
elif x % 3 == 0:
print('Divisible by 3 but not by 2')

 Explanation:
o First, it checks if x is divisible by 2.
o If x is divisible by 2, it checks whether it is also divisible by 3.
o Depending on these conditions, the program prints the appropriate message.

Using elif for Multiple Conditions

 The elif keyword stands for "else if". It's used when you want to test multiple conditions.
 The structure looks like this:

if condition1:
# Block for condition1
elif condition2:
# Block for condition2
else:
# Block for all other cases

Example: Finding the Smallest Number

Here's an example using multiple conditions with elif:

x = 5
y = 10
z = 2

if x < y and x < z:


print('x is least')
elif y < z:
print('y is least')
else:
print('z is least')

 Explanation:
o It compares x, y, and z to determine the smallest number.
o It uses the and operator to check two conditions in one line.

Understanding Execution Time

 Constant-time programs: A program where the maximum running time does not depend on the size of the
input.
o For a branching program, the maximum time it takes to run is still bounded by the number of lines
of code.
o This means that a branching program with n lines of code cannot take more than n steps to execute,
even if it contains conditionals.

Finger Exercise
Problem: Write a program that examines three variables—x, y, and z—and prints the largest odd number among
them. If none of them are odd, it should print a message to that effect.

Here’s one possible solution:

x = 3
y = 4
z = 5

# Check if x, y, or z is odd and find the largest odd number


if x % 2 != 0 and (x > y and x > z):
print(f'{x} is the largest odd number')
elif y % 2 != 0 and (y > x and y > z):
print(f'{y} is the largest odd number')
elif z % 2 != 0 and (z > x and z > y):
print(f'{z} is the largest odd number')
else:
print('None of the numbers are odd')

 Explanation:
o The program first checks if x, y, or z is odd by using the modulus operator %.
o Then, it checks if that odd number is the largest among the three.
o If none of them are odd, it prints that no odd numbers were found.

2.3. Strings and Input

📌 1. What is a String in Python?


In Python:

 A string is a sequence of characters.


 It's represented by objects of type str.
 You can use either single quotes ('...') or double quotes ("...") to define a string.

✅ Examples:
'abc' # A string using single quotes
"abc" # A string using double quotes
'123' # A string, NOT the number 123

🟡 Note: '123' is not the same as 123. One is a string of characters, the other is a number.

📌 2. Typing Expressions in the Python Interpreter


Let’s try each of the expressions from the book and understand what happens:

Expression 1:
>>> 'a'

✅ Output:

'a'

👉 This is a simple string literal. Python just echoes it.

Expression 2:
>>> 3 * 4

✅ Output:

12

👉 Multiplying two numbers — plain old arithmetic.

Expression 3:
>>> 3 * 'a'

✅ Output:

'aaa'

👉 This repeats the string 'a' 3 times. This is called string repetition.

🟡 Logic: 3 * 'a' → 'a' + 'a' + 'a' → 'aaa'

Expression 4:
>>> 3 + 4

✅ Output:

👉 Addition between two integers.

Expression 5:
>>> 'a' + 'a'

✅ Output:

'aa'
👉 This is string concatenation. It joins two strings into one.

🧠 Operator Overloading (Important Concept)


Operator Overloading means the same operator behaves differently depending on what type of data it's
used with.

Operator Used with Meaning


+ Numbers Addition
+ Strings Concatenation
* Numbers Multiplication
* String and Int Repeat string

🔁 2 * 'John' → 'JohnJohn'

🟡 Logic: Just like 2 * 3 means 3 + 3,


2 * 'John' means 'John' + 'John'

📌 3. Error Examples (Why they happen)


Expression:
>>> a

❌ Output:

NameError: name 'a' is not defined

🟡 Explanation:
Python thinks a is a variable name, not a string. Since you never defined a before, it throws an error.

✅ Fix:

>>> 'a'

Expression:
>>> 'a' * 'a'

❌ Output:

TypeError: can't multiply sequence by non-int of type 'str'

🟡 Explanation: You can only multiply a string by an integer, like 'a' * 3,


but you can’t multiply a string by another string.
📌 4. Why Type Checking is Good
Python tells you when you make a mistake by showing errors like:

 NameError
 TypeError

This is better than letting your program run incorrectly with mysterious bugs.

✅ Strong typing → safer code

📌 5. Weird but Legal Comparison:


>>> '4' < 3

✅ Output:

False

🟡 Explanation: Python says any number is considered less than any string when comparing.

But this is arbitrary and most other languages would show an error.

📌 6. Strings Are Sequences


Strings are sequences, like lists and tuples, meaning:

 They have length


 They are indexable
 They are sliceable

📏 7. Length of String: len()


>>> len('abc')

✅ Output:

🟡 'abc' has three characters: 'a', 'b', 'c'

📌 8. Indexing Strings
✅ Examples:
>>> 'abc'[0] # First character
'a'

>>> 'abc'[1] # Second character


'b'

>>> 'abc'[-1] # Last character


'c'

❌ Error:
>>> 'abc'[3]

Output:

IndexError: string index out of range

🟡 The indexes are:

'a' → index 0
'b' → index 1
'c' → index 2

So 'abc'[3] doesn't exist.

✂️ 9. Slicing Strings
Syntax:
s[start:end]

 Starts from index start


 Ends at index end - 1
 end is not included

✅ Example:
>>> 'abc'[1:3]
'bc'

🟡 Index 1 = 'b', Index 2 = 'c', Index 3 is not included.

🔄 Default Values:
>>> 'abc'[:2]
'ab' # Same as 'abc'[0:2]

>>> 'abc'[1:]
'bc' # From index 1 to end

>>> 'abc'[:]
'abc' # Full string

✅ 'abc'[:] is the same as 'abc'[0:len('abc')]

✅ Summary Table
Expression Result Meaning
'a' + 'b' 'ab' Concatenate strings
3 * 'a' 'aaa' Repeat string 3 times
len('abc') 3 Number of characters
'abc'[0] 'a' First character
'abc'[-1] 'c' Last character
'abc'[1:3] 'bc' Substring from index 1 to 2

🧠 Practice Task for You:


Try these in Python:

name = input("Enter your name: ")

print("You entered:", name)


print("Length:", len(name))
print("First character:", name[0])
print("Last character:", name[-1])
print("Twice your name:", name * 2)
print("Sliced (1:4):", name[1:4])

✅ Output (if the user enters "Priya")


Enter your name: Priya
You entered: Priya
Length: 5
First character: P
Last character: a
Twice your name: PriyaPriya
Sliced (1:4): riy

💡 Explanation:

 input() gets the name as a string.


 len(name) returns the number of characters → 5
 name[0] gives the first character → 'P'
 name[-1] gives the last character → 'a'
 name * 2 repeats the name → 'PriyaPriya'
 name[1:4] slices from index 1 to 3 → 'riy'
(index 1 = 'r', index 2 = 'i', index 3 = 'y')
2.3.1. Input in Python

🔷 1. What Is Input?
In programming, input means getting data from the user — usually via the keyboard. Python gives us built-
in functions to do this.

🔷 2. Python 2.7: raw_input() vs input()


In Python 2.7, there were two main input functions:

Function What it returns Behavior


raw_input() Always returns a string Takes user input as text, no evaluation
input() Returns evaluated result Evaluates input as a Python expression

🔸 Example in Python 2.7:


>>> name = raw_input("Enter your name: ")
Enter your name: George Washington
>>> print "Are you really", name, "?"
Are you really George Washington ?
>>> print "Are you really " + name + "?"
Are you really George Washington?

 raw_input() gets the user's input as a string.


 The first print statement adds spaces between the words.
 The second one uses string concatenation, avoiding the extra space before ?.

❗ Why Not Use input() in Python 2.7?

Because it evaluates the input. If a user types:

>>> x = input("Enter a value: ")


Enter a value: 2 + 3

Then:

 x will be 5, not the string "2 + 3"


 That could be dangerous or unexpected if a user inputs something like
__import__('os').system('rm -rf /')

🔷 3. Python 3.x: Only input() Exists


In Python 3, raw_input() is removed and input() behaves like the old raw_input().

🔹 So: input() always returns a string, no matter what the user types.

🔸 Example:
name = input("Enter your name: ")
print("Welcome,", name)

🔷 4. Example to Understand Print and Input Better


name = input("Enter your name: ")
print("Are you really", name, "?") # uses multiple arguments, adds spaces
print("Are you really " + name + "?") # uses concatenation, no extra spaces

Output:
Enter your name: Alice
Are you really Alice ?
Are you really Alice?

🔷 5. Input Always Returns a String


Let's try this:

n = input("Enter an int: ")


print(type(n))

If the user types: 3


Output:

<class 'str'>

Even though you typed a number, Python treats it as a string.

❌ So this:
print(n * 4)

Will print:

3333

Because:

 You’re repeating the string "3" four times, not multiplying the number 3.

🔷 6. Type Conversion (Casting)


To treat input as a number, you must convert it.
Conversion Function Example
To Integer int(str) int("3") → 3
To Float float(str) float("3.14") → 3.14

Example:
n = int(input("Enter an integer: "))
print(n * 4)

If user types 3, output will be:

12

Another example:
f = float(input("Enter a decimal number: "))
print("You entered:", f)
print("As integer:", int(f)) # Truncates, doesn’t round

User types 3.9 → Output:

You entered: 3.9


As integer: 3

🔷 7. Summary Table
Concept Python 2.7 Python 3.x
Input function raw_input() / input() input()
raw_input() return type str Not available
input() return type evaluated result (unsafe) str (safe, default)
Convert to int int("3") Same
Convert to float float("3.5") Same
Print with multiple args Adds space automatically Same
Print with concatenation Must manually manage spaces Same

🔷 8. Extra Practice Example


name = input("Enter your name: ")
age = int(input("Enter your age: "))

print("Hello", name + "! You will be", age + 5, "years old in 5 years.")

If you type Alice and 21, output:

Hello Alice! You will be 26 years old in 5 years.


2.4. Iteration

🔷 1. What is Iteration?
Iteration, also known as looping, is the process of repeating a set of instructions until a certain condition
is met.

Think of it like a washing machine cycle — it keeps spinning (executing) until the time runs out or the
clothes are clean (a condition becomes false).

🔁 2. Flowchart for a while loop


Here’s the generic flow:

Start
|
[Condition?] ---> No ---> Exit loop
|
Yes
|
[Do something]
|
[Go back to Condition]

This matches how the while loop works in Python.

🔷 3. Python’s while loop syntax


while condition:
# loop body (indented code)

 Python checks the condition.


 If it's True, it runs the indented block.
 Then it goes back and checks the condition again.
 This continues until the condition becomes False.

✅ 4. Example: Squaring an Integer the Hard Way


x = 3
ans = 0
itersLeft = x

while (itersLeft != 0):


ans = ans + x
itersLeft = itersLeft - 1

print(str(x) + '*' + str(x) + ' = ' + str(ans))

🔍 Step-by-step Explanation:

 x = 3 → We're trying to calculate 3 * 3.


 We initialize ans = 0 (it will hold the result).
 We initialize itersLeft = x = 3 → This is our loop counter.
 In each loop iteration:
o Add x to ans.
o Decrease itersLeft by 1.

🧠 Hand Simulation Table:

Iteration x ans itersLeft Condition (itersLeft != 0)


1 3 0 3 True
2 3 3 2 True
3 3 6 1 True
4 3 9 0 False — loop ends

Final Output:
3*3 = 9

⚠️ 5. What Happens with Negative x?


Suppose:

x = -1
itersLeft = x

Now itersLeft != 0 is True because -1 != 0. So we enter the loop:

itersLeft = itersLeft - 1 # becomes -2, then -3, etc.

It will never reach 0! ❌ The loop goes forever = infinite loop.


✅ 6. Fixing the Bug
Instead, use abs(x) to make it always positive:

x = -3
ans = 0
itersLeft = abs(x)

while (itersLeft != 0):


ans = ans + abs(x)
itersLeft = itersLeft - 1

print(str(x) + '*' + str(x) + ' = ' + str(ans))

This works for both positive and negative x, but the output will always be positive.

✍️ 7. Finger Exercise (Practice Question)


🔸 Write a program that asks the user to input 10 integers, and then prints the largest odd number
that was entered. If no odd number was entered, it should print a message to that effect.

✅ Let’s Write the Code:


largest_odd = None # To store the largest odd number

for i in range(10):
num = int(input("Enter integer #" + str(i+1) + ": "))

if num % 2 != 0: # Check if it's odd


if (largest_odd is None) or (num > largest_odd):
largest_odd = num

if largest_odd is None:
print("No odd number was entered.")
else:
print("The largest odd number entered is:", largest_odd)

🔍 Explanation:

 We run a loop 10 times using for i in range(10).


 We take user input each time, convert to int.
 If the number is odd (num % 2 != 0):
o We compare it to largest_odd.
o If largest_odd is None (first odd number), or num is greater, we update it.
 After the loop, we check:
o If no odd number was found (None), print that.
o Else, show the largest one.

🧠 Summary of Key Points from Section 2.4


Concept Explanation
Iteration Repeating code until a condition is false
while loop Executes block as long as condition is true
Infinite loop danger Happens if condition never becomes false
Hand simulation Helps understand loop behavior step-by-step
Fixing logic bugs Use abs() or control condition carefully
Practice habit Use for-loops and input to process multiple data points

5. Structured Types, Mutability, and Higher-Order


Functions
In this chapter, we delve deeper into Python’s structured types, which are types of data that allow multiple values to
be grouped together. We also explore mutability, which is the ability of an object to be modified after its creation,
and higher-order functions, which treat functions as first-class objects.

Let’s break it down step by step.

1. Scalar Types: int, float, and str

In Python, scalar types are basic types of data that hold a single value. These include:

 int: Represents integer values, such as 5, -3, or 1000.


 float: Represents floating-point numbers (i.e., decimal values), like 3.14, -0.001, or 100.0.
 str: Represents a string, which is a sequence of characters, such as "hello" or "12345".

Scalar types are immutable, meaning once you assign a value to a scalar type, that value cannot be changed. For
example:

x = 10 # x is an int
x = 20 # Now x is 20, but you can't change the value of 10 directly
Structured Type: str

 The str type is a sequence of characters.


 Indexing allows you to access individual characters in a string.

my_string = "hello"
print(my_string[0]) # Output: 'h'

 Slicing allows you to access substrings:

my_string = "hello"
print(my_string[1:4]) # Output: 'ell'

2. Structured Types in Python


Structured types allow multiple values to be stored in a single object. Python has several structured types, but we’ll
focus on three of the most common:

 Tuple: A collection of ordered elements, which is immutable (cannot be changed).


 List: A collection of ordered elements, which is mutable (can be modified).
 Dictionary (dict): A collection of key-value pairs, also mutable.

Tuples
A tuple is similar to a string, but it can store multiple types of elements, such as integers, strings, or other tuples.

 Tuple Syntax:

my_tuple = (1, 2, 3, "hello", 4.5)

 Accessing Tuple Elements: You can access elements in a tuple using indexing (just like strings).

print(my_tuple[0]) # Output: 1

 Slicing: You can slice a tuple to extract a range of elements.

print(my_tuple[1:4]) # Output: (2, 3, 'hello')

 Immutability: You cannot modify a tuple after it is created. For example:

my_tuple[0] = 10 # This will raise a TypeError

Lists
A list is an ordered collection of elements, and it is mutable, meaning you can modify, add, or remove elements from
it.

 List Syntax:

my_list = [1, 2, 3, "hello", 4.5]

 Accessing List Elements: Like tuples, you can access elements using indexing.

print(my_list[0]) # Output: 1

 Slicing: Lists can also be sliced to extract a subset of elements.

print(my_list[1:4]) # Output: [2, 3, 'hello']

 Mutability: Unlike tuples, lists are mutable, so you can change, add, or remove elements.

my_list[0] = 10 # Changes the first element to 10


my_list.append(6) # Adds 6 to the end of the list
print(my_list) # Output: [10, 2, 3, 'hello', 4.5, 6]

Dictionaries (dict)
A dictionary is an unordered collection of key-value pairs. Each key in the dictionary maps to a corresponding value.

 Dictionary Syntax:
my_dict = {'name': 'Alice', 'age': 25, 'city': 'New York'}

 Accessing Dictionary Values: You access values by referring to their associated key.

print(my_dict['name']) # Output: 'Alice'

 Adding or Modifying Entries: You can modify or add key-value pairs in a dictionary.

my_dict['age'] = 26 # Changes the value of 'age'


my_dict['country'] = 'USA' # Adds a new key-value pair

 Mutability: Dictionaries are mutable, allowing you to add, modify, or delete key-value pairs.

del my_dict['city'] # Removes the 'city' key-value pair

3. Mutability in Python

Mutability refers to whether the contents of an object can be changed after its creation. Here’s the distinction
between mutable and immutable objects:

 Immutable Objects: These cannot be modified after creation. Examples are int, float, str, and tuple.

x = 5
# x = 10 # This will reassign x, but 5 cannot be changed directly

 Mutable Objects: These can be modified after creation. Examples include list, dict, and set.

my_list = [1, 2, 3]
my_list[0] = 10 # This modifies the list
print(my_list) # Output: [10, 2, 3]

This distinction is important because mutable objects allow for more flexibility but also carry risks of accidental
modification (e.g., if shared between functions).

4. Higher-Order Functions

In Python, functions are first-class objects, meaning they can be assigned to variables, passed as arguments, or
returned from other functions. This is the basis for higher-order functions.

A higher-order function is a function that either:

1. Takes one or more functions as arguments.


2. Returns a function as its result.

Common Higher-Order Functions in Python:

 map(): Applies a function to all items in an iterable (like a list) and returns an iterator of the results.

numbers = [1, 2, 3, 4]
squares = map(lambda x: x ** 2, numbers)
print(list(squares)) # Output: [1, 4, 9, 16]

 filter(): Filters elements of an iterable by applying a function to each element and only including those for
which the function returns True.
numbers = [1, 2, 3, 4, 5]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers)) # Output: [2, 4]

 reduce() (from functools module): Reduces an iterable to a single value by applying a function
cumulatively.

from functools import reduce


numbers = [1, 2, 3, 4]
product = reduce(lambda x, y: x * y, numbers)
print(product) # Output: 24
Custom Higher-Order Function:

You can create your own higher-order function that accepts other functions as arguments:

def apply_function(func, data):


return [func(x) for x in data]

# Example usage:
result = apply_function(lambda x: x ** 2, [1, 2, 3, 4])
print(result) # Output: [1, 4, 9, 16]

Summary

 Scalar Types: Basic types like int, float, and str, which hold a single value and are immutable.
 Structured Types: More complex types like tuple, list, and dict. Tuples are immutable, while lists and
dictionaries are mutable.
 Mutability: Mutable objects can be changed after creation, while immutable objects cannot.
 Higher-Order Functions: Functions that can accept other functions as arguments or return functions. These
provide flexibility and reusability.

This chapter sets the foundation for working with more advanced data structures in Python, including tuples, lists,
dictionaries, and higher-order functions. These concepts are crucial for writing clean and efficient code in Python.

5.1. Tuples
A tuple in Python is an ordered collection of elements (just like a list). However, the key difference between a tuple
and a list is that tuples are immutable. This means that once you create a tuple, you cannot modify it (no adding,
removing, or changing elements).

Key Characteristics of Tuples:

 Ordered: The elements in a tuple have a specific order, which means the position of each element matters.
 Heterogeneous: A tuple can store elements of different types (e.g., integers, strings, floats, etc.), unlike lists
which are usually homogeneous.
 Immutable: Once a tuple is created, you cannot modify it (no changes allowed).

Creating a Tuple

You can create a tuple by writing a comma-separated list of elements inside parentheses ().
Example 1: Creating an Empty Tuple
t1 = () # This is an empty tuple.
print(t1) # Outputs: ()

In this case, the tuple t1 doesn't contain any elements, so it prints an empty tuple ().

Example 2: Creating a Tuple with Mixed Data Types


t2 = (1, 'two', 3)
print(t2) # Outputs: (1, 'two', 3)

In this case, the tuple t2 contains three elements:

 An integer: 1
 A string: 'two'
 Another integer: 3

Single-Element Tuples

In Python, you might think that creating a tuple with one element would look like this:

singleton_tuple = (1)

However, this is not a tuple. It's just the integer 1 inside parentheses, which is not a tuple.

To create a tuple with a single element, you must include a comma after the element:

singleton_tuple = (1,) # This is a tuple with one element.

The comma tells Python that you're creating a tuple, not just grouping the value.

Tuple Operations

1. Concatenation: You can combine two tuples using the + operator, which concatenates them.

t1 = (1, 'two', 3)
t2 = (t1, 3.25)
print(t2) # Outputs: ((1, 'two', 3), 3.25)

In the above example, t2 is a tuple containing:

o The tuple t1 (which itself contains 1, 'two', 3)


o The float 3.25

You can also concatenate two tuples to make a new, longer tuple:

print(t1 + t2) # Outputs: (1, 'two', 3, (1, 'two', 3), 3.25)

This combines the elements of t1 and t2, resulting in a new tuple with five elements.

2. Indexing: Just like lists, you can access individual elements of a tuple using their index. Remember, Python
uses zero-based indexing, so the first element has index 0, the second has index 1, and so on.

print((t1 + t2)[3]) # Outputs: (1, 'two', 3)

In this case, t1 + t2 creates a tuple with five elements, and the code [(t1 + t2)[3]] retrieves the 4th
element (which is (1, 'two', 3)).
3. Slicing: You can slice a tuple to extract a part of it. The slicing syntax is:

tuple[start:stop]

Here’s how it works:

print((t1 + t2)[2:5]) # Outputs: (3, (1, 'two', 3), 3.25)

This code slices the concatenated tuple from index 2 to index 4 (index 5 is not included). The result is a sub-
tuple containing three elements.

Iterating Over Tuples

You can iterate over the elements of a tuple using a for loop. This is useful when you want to perform an operation
on each element in the tuple.

Example: Finding common divisors of two numbers.

def findDivisors(n1, n2):


"""Returns a tuple containing all common divisors of n1 and n2."""
divisors = () # Start with an empty tuple
for i in range(1, min(n1, n2) + 1): # Loop from 1 to the smallest of n1 and n2
if n1 % i == 0 and n2 % i == 0: # If i divides both numbers
divisors = divisors + (i,) # Add i to the tuple
return divisors

divisors = findDivisors(20, 100)


print(divisors) # Outputs: (1, 2, 4, 5, 10, 20)

In this example:

 The function findDivisors finds the common divisors of n1 and n2 and returns them in a tuple.
 A for loop is used to check if each number i divides both n1 and n2. If it does, i is added to the tuple
divisors.
 The final result is printed as a tuple of common divisors: (1, 2, 4, 5, 10, 20).

You can also sum all the divisors found:

total = 0
for d in divisors:
total += d # Add each divisor to total
print(total) # Outputs: 42

Summary of Key Points:

 Tuples are like lists but immutable (you can't modify them after creation).
 They are ordered collections of elements that can be of any data type.
 To create a single-element tuple, always include a comma after the element.
 Tuples can be concatenated, indexed, and sliced.
 You can iterate over a tuple’s elements using a for loop.
 Tuples can hold multiple types of elements, and you can even have a tuple inside another tuple (nested
tuples).

5.1.1. Sequences and Multiple Assignment


In Python, multiple assignment allows you to unpack values from a sequence (like a tuple, list, or string) directly into
multiple variables in one statement. This makes it easy to extract and assign values from a sequence without needing
extra lines of code.

How Multiple Assignment Works:

When you know the number of elements in a sequence, you can unpack it into separate variables using Python's
multiple assignment syntax. Here’s how it works with tuples and strings:

Example with a Tuple:


x, y = (3, 4)

In this example:

 x will be assigned the value 3


 y will be assigned the value 4

This means that the values from the tuple (3, 4) are unpacked and assigned to x and y.

Example with a String:


a, b, c = 'xyz'

In this example:

 a will be assigned the value 'x'


 b will be assigned the value 'y'
 c will be assigned the value 'z'

This is another example of unpacking a sequence ('xyz') into individual characters.

Why Use Multiple Assignment?

Multiple assignment is very convenient when working with functions that return sequences. Instead of manually
accessing each element of the returned sequence, you can unpack them directly into variables in one line.

Example: Using Multiple Assignment with Functions

Here’s a practical example using a function that returns multiple values as a tuple:

Function: findExtremeDivisors

This function finds the smallest common divisor greater than 1 and the largest common divisor of two given
numbers (n1 and n2).

def findExtremeDivisors(n1, n2):


"""Assumes that n1 and n2 are positive ints
Returns a tuple containing the smallest common divisor > 1 and the largest common
divisor of n1 and n2"""

divisors = () # Initialize an empty tuple to store divisors


minVal, maxVal = None, None # Initialize variables for smallest and largest
divisors

# Loop through all numbers from 2 to the smallest of n1 or n2


for i in range(2, min(n1, n2) + 1):
# Check if 'i' is a common divisor of both n1 and n2
if n1 % i == 0 and n2 % i == 0:
# Update the smallest common divisor if 'i' is smaller than current minVal
if minVal is None or i < minVal:
minVal = i
# Update the largest common divisor if 'i' is larger than current maxVal
if maxVal is None or i > maxVal:
maxVal = i

# Return a tuple with the smallest and largest common divisors


return (minVal, maxVal)
Function Explanation:

 The function findExtremeDivisors(n1, n2) loops through integers starting from 2 and checks if they
are divisors of both n1 and n2.
 For each common divisor, the function updates the smallest (minVal) and largest (maxVal) divisors found
so far.
 It finally returns a tuple containing the smallest and largest common divisors.

Using Multiple Assignment with This Function:

You can use multiple assignment to unpack the result of this function into two variables:

minDivisor, maxDivisor = findExtremeDivisors(100, 200)

Here’s what happens:

 The function call findExtremeDivisors(100, 200) returns a tuple (2, 100), where 2 is the smallest
common divisor and 100 is the largest common divisor.
 The multiple assignment minDivisor, maxDivisor = findExtremeDivisors(100, 200) unpacks
this tuple:
o minDivisor gets the value 2
o maxDivisor gets the value 100

So, after executing this statement, the variables minDivisor and maxDivisor are assigned the respective values
from the tuple.

Why This Is Useful:

 Convenience: Multiple assignment simplifies extracting values from tuples, lists, or strings.
 Readability: Instead of accessing each element of a sequence separately, you can directly unpack them into
variables.
 Efficient: This feature makes it easy to handle functions that return multiple values, especially when working
with fixed-size sequences like tuples.

Conclusion:

Multiple assignment is a simple but powerful feature in Python that allows you to directly unpack elements from
sequences like tuples or strings into multiple variables. It's particularly useful when working with functions that
return multiple values as a tuple, like the findExtremeDivisors function. This makes your code cleaner and more
readable.

5.2. Lists and Mutability


In Python, lists are a type of ordered collection of values. Lists are similar to tuples in that they both store sequences
of elements, but there is one crucial difference: lists are mutable, while tuples and strings are immutable.

Lists and Their Syntax

A list in Python is created using square brackets []. For example:

L = ['I did it all', 4, 'love']

This list L contains three elements: a string 'I did it all', an integer 4, and another string 'love'.

 To access each element in the list, you can use an index. The index starts from 0 (zero) and increments by 1
for each subsequent element.

Example code:

L = ['I did it all', 4, 'love']


for i in range(len(L)):
print(L[i])

This will output:

I did it all
4
love
Indexing and Slicing Lists

Just like tuples, lists support indexing, where you can access individual elements by their position. Slicing also works
with lists, which allows you to access a range of elements. However, in the case of lists, because they are mutable,
you can modify the list after slicing.

For example:

# Example of slicing
result = [1, 2, 3, 4][1:3][1]
print(result) # Output: 3

In the code above:

 [1, 2, 3, 4] creates a list.


 [1:3] slices it from index 1 to index 3 (excludes 3), resulting in [2, 3].
 [1] gets the second element of [2, 3], which is 3.

Lists vs. Tuples: Mutability

The main difference between lists and tuples is that lists are mutable, meaning that you can modify them after
creation, while tuples and strings are immutable and cannot be altered once they are created.

For example:

 Immutable Object: A tuple cannot be changed. If you try to modify its elements, Python will raise an error.
 Mutable Object: A list, on the other hand, can be modified. You can change, add, or remove elements in a
list after it has been created.
Aliasing and Mutability

When a list is assigned to another variable, both variables refer to the same object in memory. This phenomenon is
called aliasing.

Example:

Techs = ['MIT', 'Caltech']


Ivys = ['Harvard', 'Yale', 'Brown']
Univs = [Techs, Ivys]

In this case, Univs is a list containing two lists: Techs and Ivys. However, when we modify Techs, we also modify
the first element of Univs since both Techs and the first element of Univs refer to the same list.

For instance, let's add an element to Techs:

Techs.append('RPI')
print('Univs =', Univs)

This will output:

Univs = [['MIT', 'Caltech', 'RPI'], ['Harvard', 'Yale', 'Brown']]

As you can see, the change made to the list Techs is reflected in Univs, because both variables point to the same
list object.

To check whether two lists are pointing to the same object in memory, we can use the built-in id() function, which
returns the unique identifier of an object:

print(id(Univs)) # Unique ID of Univs


print(id(Univs[0])) # Unique ID of the first list in Univs
The id Function

The id() function helps confirm whether two variables point to the same object. It is crucial for understanding how
mutability works in Python. For example, if we print the IDs of two lists:

Univs = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]


Univs1 = [['MIT', 'Caltech'], ['Harvard', 'Yale', 'Brown']]

print(Univs == Univs1) # True, because the lists have the same values
print(id(Univs) == id(Univs1)) # False, because they are two distinct objects

This will output:

True
False

Although Univs and Univs1 have the same values (i.e., they contain the same lists), they are two distinct objects in
memory, as evidenced by their different id() values.

Mutating Lists

Because lists are mutable, you can perform operations that modify them directly. For example, the append()
method adds a new element to the end of a list:

Techs.append('RPI')

This directly changes the original Techs list. Unlike immutable types like tuples, you don't create a new object when
you modify a list; you change the existing one.

List Methods

Python provides a variety of methods to manipulate lists. Some of the most common ones include:

 append(e): Adds an element e to the end of the list.


 count(e): Returns the number of times e appears in the list.
 insert(i, e): Inserts element e at index i in the list.
 extend(L1): Adds all elements of list L1 to the end of the current list.
 remove(e): Removes the first occurrence of element e from the list.
 index(e): Returns the index of the first occurrence of e in the list. Raises an exception if e is not found.
 pop(i): Removes and returns the element at index i. If i is not provided, it removes and returns the last
element.
 sort(): Sorts the list in ascending order.
 reverse(): Reverses the order of elements in the list.

Example of List Methods in Action:


L1 = [1, 2, 3]
L2 = [4, 5, 6]

# Using + operator to concatenate lists (no side effect)


L3 = L1 + L2
print(L3) # Output: [1, 2, 3, 4, 5, 6]

# Using extend() to add elements of L2 to L1 (mutates L1)


L1.extend(L2)
print(L1) # Output: [1, 2, 3, 4, 5, 6]

# Using append() to add a list as a single element


L1.append(L2)
print(L1) # Output: [1, 2, 3, 4, 5, 6, [4, 5, 6]]

In the code above:

 The + operator creates a new list L3 without modifying L1 or L2.


 The extend() method adds the elements of L2 to the end of L1, modifying L1.
 The append() method adds L2 as a single element at the end of L1.

Mutability and Side Effects

Many list methods, such as append(), extend(), and sort(), have side effects: they modify the list in place.
However, some operations like + and slice() return a new list without modifying the original.
Conclusion

 Lists are mutable, allowing you to modify their elements after creation.
 The mutability of lists can lead to aliasing, where multiple variables refer to the same object, which can lead
to unexpected changes in the list.
 Python provides several methods for modifying and querying lists, but it's important to be mindful of the
side effects caused by mutating operations.

5.2.1. Cloning (Copying a List)


🔍 Problem Statement:

Mutating (changing) a list while iterating over it can cause unexpected results.

Let’s take the example again:

def removeDups(L1, L2):


"""Removes any element from L1 that also occurs in L2"""
for e1 in L1:
if e1 in L2:
L1.remove(e1)

Then we run:

L1 = [1, 2, 3, 4]
L2 = [1, 2, 5, 6]
removeDups(L1, L2)
print('L1 =', L1)

🔴 Unexpected Output:
L1 = [2, 3, 4]

Wait, why? 😕

🔎 Behind the Scenes (Python’s Internal Counter):

Let’s simulate how Python runs the loop:

 Start: L1 = [1, 2, 3, 4]
 First element e1 = 1 → Found in L2 → Remove 1 → L1 = [2, 3, 4]
 Python’s internal counter moves to next index (1), but now L1[1] is 3 (not the original 2!)
 So 2 is skipped

Hence the mistake: mutating the list during iteration skips elements

✅ Proper Solution: Clone the List First


def removeDups(L1, L2):
for e1 in L1[:]: # this creates a shallow copy
if e1 in L2:
L1.remove(e1)

Now it works! Because you’re looping over a copy of L1, not L1 itself.

📌 Why L1[:] Works:

 L1[:] means: “Take all elements from start to end” → a new copy of the list
 Changing L1 now doesn’t affect what you're iterating over

⚠️ Why this won’t work:


newL1 = L1

This just gives a new name for the same list — both L1 and newL1 refer to the same object. So if you
mutate L1, newL1 is also affected. It’s not a copy, just a different label.

🔄 Other Ways to Clone a List:

Method Description Example


L1[:] Slice copy copy1 = L1[:]
list(L1) Constructor copy copy2 = list(L1)
copy.copy(L1) Shallow copy (one level deep)
copy.deepcopy(L1) Deep copy (for nested lists)

🧠 Example of Deep Copy:


import copy

original = [[1, 2], [3, 4]]


cloned = copy.deepcopy(original)
cloned[0][0] = 99

print(original) # [[1, 2], [3, 4]]


print(cloned) # [[99, 2], [3, 4]]

If you used shallow copy here, both would change together — which is dangerous when working with
nested or mutable structures.

💡 Summary of 5.2.1
Concept Explanation
Mutating list during loop Can cause skipped elements or bugs
Concept Explanation
Safe way to loop Clone the list first using L1[:]
Why not use newL1 = L1? It’s not a clone, just a new name
Use copy.deepcopy() When the list has lists or mutable objects inside

5.2.2. List Comprehension


List comprehension is Python's short, elegant way to create new lists from existing sequences.

🧠 Basic Syntax:
[expression for variable in iterable]

This will:

 Take each value from iterable


 Apply the expression
 Collect the result in a new list

🔹 Example:
L = [x**2 for x in range(1, 7)]
print(L)

🔸 Output:
[1, 4, 9, 16, 25, 36]

It’s equivalent to:

L = []
for x in range(1, 7):
L.append(x**2)

✅ Adding Conditions with if

You can filter the values before applying the expression.

mixed = [1, 2, 'a', 3, 4.0]


result = [x**2 for x in mixed if type(x) == int]
print(result)

🔸 Output:
[1, 4, 9]

🔍 Why?

 The loop ignores anything that's not an int


 Then squares the rest

🧠 Advanced: Multiple for and if


matrix = [[1, 2], [3, 4]]
flattened = [item for row in matrix for item in row]
print(flattened)

Output:

[1, 2, 3, 4]

💡 Nested Comprehension:

You can even write:

[x for x in range(10) if x % 2 == 0 if x > 4]

Which gives:

[6, 8]

But avoid making them too complex, because:

❗ "Subtle is not usually a desirable property"

Readable code > Fancy one-liners

✅ Summary of 5.2.2
Topic Summary
List comprehension A short way to build lists
Syntax [expression for variable in iterable]
Add filters Use if to include only certain elements
Nested loops You can include more than one for, but keep it readable

5.4. Strings, Tuples, and Lists — In Detail


Python provides three main sequence types:

 str → String (for text)


 tuple → Tuple (fixed collection of items)
 list → List (modifiable collection of items)

These are sequence types because they hold items in ordered fashion and allow index-based access.

🔁 1. Common Operations on Sequences (Figure 5.6)

All three types (str, tuple, list) support the following operations:

Syntax Meaning Example Output

seq[i] Access item at index i 'abc'[1] 'b'

len(seq) Length of sequence len([1,2,3]) 3

seq1 + seq2 Concatenate sequences [1,2] + [3,4] [1,2,3,4]

n * seq Repeat sequence n times 2 * 'hi' 'hihi'

seq[start:end] Slice from start to end-1 'python'[1:4] 'yth'

e in seq Membership test 2 in (1,2,3) True

e not in seq Negative membership 'a' not in 'cat' False

for e in seq: Loop through items for c in 'abc': print(c) Prints a, b, c

All three sequence types behave similarly here, but differ in:

 What they can store


 Whether they can be modified

🧠 2. Comparison of Sequence Types (Figure 5.7)

Feature str tuple list

Elements Only characters Any type Any type

Literal Example 'abc' (1, 'a') [1, 'a']

Mutable (Changeable)? ❌ No ❌ No ✅ Yes

Can be dictionary key? ✅ Yes ✅ Yes ❌ No

Versatility Least Medium Most

Usage Text operations Group fixed data Modify data dynamically

🧠 3. Why Are Lists More Common?


 Lists are mutable, meaning you can:
o Add items (.append(), .insert())
o Remove items (.remove(), .pop())
o Change items by index (L[2] = 10)
 Programmers often need to build up or change data — lists are ideal.

📌 Example: Building a List of Even Numbers

L = [1, 2, 3, 4, 5, 6]
evenElems = []
for e in L:
if e % 2 == 0:
evenElems.append(e)
print(evenElems) # Output: [2, 4, 6]

You cannot do this with a tuple because tuples are immutable.

🔐 4. Why Use Tuples?

 Tuples are immutable, so:


o No risk of accidental change or aliasing
o Can be used as dictionary keys
o Slightly more efficient (used in fixed settings)

📌 Example:

coordinates = (10.5, 20.3) # Fixed location data


person_info = {("John", "Doe"): 45} # Tuple used as dict key

✂️ 5. Strings: Useful Built-in Methods (Figure 5.8)

Strings (str) have many powerful methods. But remember: strings are immutable — methods return new strings,
not modify originals.

Method Purpose Example Output

s.count(s1) Count occurrences 'banana'.count('a') 3

s.find(s1) Index of first s1, -1 if not found 'banana'.find('na') 2

s.rfind(s1) From right (last occurrence) 'banana'.rfind('na') 4

Like find, but raises error if not


s.index(s1) 'abc'.index('b') 1
found

Like rfind, but raises error if not


s.rindex(s1) 'abcabc'.rindex('a') 3
found

s.lower() Convert to lowercase 'PYTHON'.lower() 'python'

s.replace(old, 'apple'.replace('p',
Replace substrings 'abble'
new) 'b')
Method Purpose Example Output

s.rstrip() Remove trailing spaces 'hello '.rstrip() 'hello'

['a', 'b',
s.split(d) Split by delimiter d 'a b c'.split(' ')
'c']

📌 Special Note:

 split() can break strings into lists of words or items.

sentence = 'David Guttag plays basketball'


words = sentence.split(' ')
print(words)
# ['David', 'Guttag', 'plays', 'basketball']

💡 Summary

Feature str tuple list

Data Type Characters Any Any

Mutable? ❌ ❌ ✅

Ordered? ✅ ✅ ✅

Indexable? ✅ ✅ ✅

Can hold mixed types? ❌ ✅ ✅

Can change size? ❌ ❌ ✅

Can be used as dict key? ✅ ✅ ❌

Built-in methods Many (for text) Few Many

Use Case Text processing Fixed data (e.g., coordinates) Modifiable collections

🧠 Pro Tips

 Use list if you need to change elements or grow/shrink the sequence.


 Use tuple when you want data to be protected from changes.
 Use str for handling characters and text; leverage built-in string methods.

🔹 1. What Are Sequence Types?


Strings, tuples, and lists are all sequence types in Python. That means they:
 Contain multiple items.
 Keep the items in order.
 Support operations like indexing, slicing, and looping.

🔤 2. String (str)
📌 Definition:

A string is an immutable sequence of characters.

✅ Features:

 Can hold only text (characters).


 Cannot be changed after creation (immutable).
 Has many built-in methods for manipulation.

🔍 Example:
s = "Hello"
print(s[1]) # e
print(s + " World") # Hello World
print(s.upper()) # HELLO

🔧 Common String Methods:

Method Description Example Output


s.count(x) Count occurrences "banana".count('a') 3
s.find(x) First index of x "banana".find('na') 2
s.replace(a, b) Replace a with b "apple".replace('p', 'b') "abble"
s.lower() To lowercase "HI".lower() "hi"
s.upper() To uppercase "hi".upper() "HI"
s.split(' ') Splits string into list "a b c".split() ['a', 'b', 'c']

❌ Immutable Example:
s = "Hello"
s[0] = "h" # ❌ Error! Strings cannot be modified.

🔗 3. Tuple (tuple)
📌 Definition:

A tuple is an immutable sequence that can contain any type of data.

✅ Features:

 Can hold multiple data types.


 Immutable (once created, cannot change).
 Can be used as a key in dictionaries.

🔍 Example:
t = (1, "apple", 3.14)
print(t[0]) # 1

⚠️ Singleton Tuple:
single = (3,) # Must include comma to be a tuple

❌ Immutable Example:
t = (1, 2, 3)
t[0] = 10 # ❌ Error! Tuples are immutable.

✅ Use in Dictionaries:
location = (19.07, 72.87)
data = {location: "Mumbai"}

📋 4. List (list)
📌 Definition:

A list is a mutable sequence that can contain any type of data.

✅ Features:

 Can grow or shrink.


 Supports item assignment and deletion.
 Most flexible and commonly used.

🔍 Example:
fruits = ["apple", "banana", "cherry"]
print(fruits[1]) # banana
fruits[1] = "kiwi" # ✅ You can modify
print(fruits) # ['apple', 'kiwi', 'cherry']

🧠 Useful List Methods:

Method Description Example Output


append(x) Add to end L.append(5) Adds 5
insert(i, x) Add at index L.insert(1, 'a') Adds at position 1
remove(x) Remove first match L.remove('a') Removes 'a'
pop(i) Remove and return L.pop(2) Removes at index 2
sort() Sorts the list L.sort() Sorted list
reverse() Reverses the list L.reverse() Reversed list
🔁 5. Common Operations on All Sequences
These work on str, tuple, and list:

Expression Description Example Output


seq[i] Access by index "cat"[1] 'a'
len(seq) Length of sequence len([1, 2]) 2
seq1 + seq2 Concatenation [1] + [2] [1, 2]
n * seq Repetition 3 * 'ha' 'hahaha'
seq[start:end] Slice 'hello'[1:4] 'ell'
e in seq Membership test 'a' in 'cat' True
for e in seq Loop through items for x in [1,2]: print(x) Prints 1, 2

📊 6. Comparison Table
Feature str tuple list

Mutable ❌ ❌ ✅
Can hold multiple types ❌ (only characters) ✅ ✅
Can be dictionary key ✅ ✅ ❌
Common use Text Fixed data Dynamic data
Has built-in methods ✅ (many) Few ✅ (many)

🔎 7. Example Use Cases


Task Use
Store full name str
Store coordinates tuple
Store shopping cart items list
Store date of birth tuple
Store user input from text field str
Store changing list of users list

🧠 Conclusion
 Use strings for text, where characters don’t change.
 Use tuples when you need an unchangeable, fixed group of values.
 Use lists when you need a dynamic, flexible container.
5.5. Dictionaries
🔑 What is a Dictionary in Python?
A dictionary (dict) in Python is a mutable, unordered collection of key-value pairs.
Think of it like a real-life dictionary: you look up a word (key), and get its meaning (value).

monthNumbers = {'Jan': 1, 'Feb': 2, 'Mar': 3}

 Keys: 'Jan', 'Feb', 'Mar'


 Values: 1, 2, 3

You can also use numbers as keys:

monthNumbers[1] # Returns 'Jan' if defined

🛠️ Creating and Using a Dictionary


monthNumbers = {
'Jan':1, 'Feb':2, 'Mar':3, 'Apr':4, 'May':5,
1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May'
}

print('The third month is', monthNumbers[3])

Output:

The third month is Mar

⚠️ Important Notes:
 Dictionaries are unordered → You cannot access elements using an index like d[0].
 Each key must be immutable (e.g., strings, numbers, or tuples).
 Keys are unique, values can repeat.

🔁 Looping Through a Dictionary


keys = []
for key in monthNumbers:
keys.append(key)
keys.sort()
print(keys)

✨ Useful Dictionary Methods


Method Description
len(d) Returns the number of items
Method Description
d.keys() List of all keys
d.values() List of all values
k in d Checks if key k exists
d[k] Value for key k
d.get(k, v) Gets d[k], or v if k not found
d[k] = v Sets key k to value v
del d[k] Removes key k

📖 Example: Word Translator


EtoF = {'bread':'pain', 'wine':'vin', 'I':'Je', 'drink':'bois'}
FtoE = {'pain':'bread', 'vin':'wine', 'Je':'I', 'bois':'drink'}
dicts = {'English to French':EtoF, 'French to English':FtoE}
python
CopyEdit
def translateWord(word, dictionary):
if word in dictionary:
return dictionary[word]
elif word != '':
return '"' + word + '"'
return word
python
CopyEdit
def translate(phrase, dicts, direction):
dictionary = dicts[direction]
translation = ''
word = ''
for c in phrase:
if c.isalpha():
word += c
else:
translation += translateWord(word, dictionary) + c
word = ''
return translation + ' ' + translateWord(word, dictionary)

Usage:
print(translate('I drink good red wine, and eat bread.', dicts, 'English to French'))

⚠️ Be Careful: Dictionaries are Mutable


FtoE['bois'] = 'wood'
print(translate('Je bois du vin rouge.', dicts, 'French to English'))

Output:

I wood of wine red.

🧠 Dictionaries with Tuples as Keys


Tuples (which are immutable) can be used as keys:
flightInfo = {('AI202', 'Mon'): '10:00 AM', ('AI202', 'Tue'): '11:00 AM'}
print(flightInfo[('AI202', 'Mon')])

❌ Why Not Use a List of Pairs Instead?


You can create a dictionary-like structure with lists:

def keySearch(L, k):


for elem in L:
if elem[0] == k:
return elem[1]
return None

But it’s slow since it checks each pair linearly.

🚀 Python Dictionaries Are Fast!


They use a method called hashing, which allows almost instant lookup, even in large datasets.

Unit-2 Functions, Exception, Modules and Files


Functions: Difference between a Function and a Method, Defining a
Function, Calling a Function, Returning Results from a Function, Returning
Multiple Values from a Function, Functions are First Class Objects, Pass by
Object Reference, Formal and Actual Arguments, Positional Arguments,
Keyword Arguments, Default Arguments, Variable Length Arguments, Local
and Global Variables, The Global Keyword, Passing a Group of Elements to a
Function, Recursive Functions, Anonymous Functions or Lambdas (Using
Lambdas with filter() Function, Using Lambdas with map() Function, Using
Lambdas with reduce() Function), Function Decorators, Generators, Structured
Programming, Creating our Own Modules in Python, The Special Variable name

Exceptions: Errors in a Python Program (Compile-Time Errors, Runtime


Errors, Logical Errors), Exceptions, Exception Handling, Types of Exceptions,
The Except Block, the assert Statement, User- Defined Exceptions, Logging the
Exceptions
Files: Files, Types of Files in Python, Opening a File, Closing a File,
Working with Text Files Containing Strings, Knowing Whether File Exists or
Not, Working with Binary Files, The with Statement, Pickle in Python, The
seek() and tell() Methods

Functions
Functions in Python
A function is like a mini program within a program. It contains a group of statements designed to perform a
specific task. Functions are useful when you have multiple tasks to do in a program. Instead of writing the
same code again and again, you can create functions to handle those tasks.

Python already has some built-in functions to do common tasks:

 print() to display output


 sqrt() to calculate the square root
 power() to calculate powers

In addition to these, you can also create your own functions, called user-defined functions.

Advantages of Functions:

1. Task Handling: Functions are great for processing data, making calculations, or performing any task
in software development.
2. Reusability: Once you write a function, you can use it multiple times, saving you from rewriting the
same code. This avoids code repetition.
3. Modularity: Functions break down a large task into smaller sub-tasks. Each sub-task can be handled
by a separate function, making the code more organized and manageable.
4. Easy Maintenance: If you need to add new features to a program, you can simply write a new
function. If a feature is no longer needed, you can remove the related function without affecting the
rest of the program.
5. Easier Debugging: If there’s an error, you can fix the issue in just the relevant function without
affecting the rest of the code.
6. Shorter Programs: Using functions helps reduce the overall length of the program, making it more
efficient.

Function vs. Method


Both functions and methods are similar because they are blocks of code that perform specific tasks.
However, there is a key difference in where they are defined and how they are called.

Function:

 A function is a block of code that is designed to perform a specific task.


 It can be written anywhere in a Python program, either inside or outside a class.
 Functions are called using their name.
Example:

def greet():
print("Hello, World!")

greet() # Calling the function

 The function is independent and doesn't belong to any object or class.

Method:

 A method is a special type of function that is defined inside a class.


 Methods are associated with an object (instance of a class) or the class itself.
 You call a method by using either the object or the class name.

Examples:

1. Calling via an Object:

class MyClass:
def greet(self):
print("Hello from the method!")

obj = MyClass() # Creating an object of the class


obj.greet() # Calling the method using the object

2. Calling via the Class Name:

class MyClass:
@staticmethod
def greet():
print("Hello from the static method!")

MyClass.greet() # Calling the method using the class name

Key Differences:

Aspect Function Method


Definition
Defined outside a class Defined inside a class
Location
Called using object.method() or
How it's Called Called using the function name
Class.method()
Independent and not tied to any object Associated with an object (instance) or the
Association
or class class itself

Summary:

 A function is a standalone block of code.


 A method is a function that is inside a class and is associated with an object or the class itself.

When you create your own functions, you define them first and then call them by their name. When a
function is inside a class, it becomes a method, and you call it using either an object or the class name.
Defining a Function in Python:
In Python, you define a function using the def keyword, followed by the function name, parentheses (which may
include parameters), and a colon to indicate the start of the function's body.

Syntax for Function Definition:


def function_name(parameters):
# Function body (statements)

 def: This is the keyword used to define a function.


 function_name: The name you choose for the function.
 parameters: These are the variables inside the parentheses. They allow the function to accept data (called
arguments) when it is called.
 Colon (:): It indicates that the function body is starting.
 Function body: This is the part where you define the logic of the function.

Example:

Let's write a function that adds two numbers:

def sum(a, b):


""" This function finds the sum of two numbers """
c = a + b
print(c)

Explanation of Each Part:

1. def sum(a, b):


o def: Marks the beginning of the function definition.
o sum: The function name, which tells us that this function will likely deal with summing values.
o (a, b): These are parameters of the function. When you call this function, you'll need to provide
two values (arguments) that will be assigned to a and b.
o Colon (:): Signals the start of the function's body, where the actual work is done.
2. Docstring (Optional):

""" This function finds the sum of two numbers """

o This is a docstring. It's a short description that explains what the function does.
o It is written between triple quotes (""" or '''), which allows for multi-line comments or
descriptions.
o Docstrings are optional, but it's a good habit to include them to describe what the function does.
3. Function Logic:

c = a + b
print(c)

o c = a + b: The function calculates the sum of the values passed to a and b, and stores the result in
c.
o print(c): This line displays the result of the sum to the screen.

Calling the Function:

To use this function, you need to call it with two numbers as arguments:

sum(3, 5)
Output:

Key Points:

1. Parameters: Variables like a and b inside the parentheses are called parameters. They are placeholders for
the actual values (arguments) passed to the function when it's called.
2. Function Body: The body of the function contains the logic (the code that gets executed) and should be
properly indented.
3. Docstrings: While optional, it's good practice to write docstrings at the start of the function to explain its
purpose.
4. Indentation: All statements inside the function should be indented. This is a fundamental rule in Python.

Summary:

 def: Defines a function.


 Function Name: The name given to the function (e.g., sum).
 Parameters: Variables inside parentheses that the function uses to receive values.
 Function Body: The indented block of code that defines what the function does.
 Docstring: A description (optional) of what the function does.

Calling a Function in Python


A function in Python doesn't run on its own. It only executes when called. Calling a function means
invoking it by its name and passing any necessary arguments to it. Here's a detailed explanation:

Steps to Call a Function:

1. Function Name: To call a function, simply use its name.


2. Arguments: Inside the parentheses, pass the required values (arguments) that the function needs to
perform its task.

Example:

Let's look at the sum function that we defined earlier:

def sum(a, b):


"""This function finds the sum of two numbers."""
c = a + b
print('Sum= ', c)

To call this function, we use:

sum(10, 15)

How It Works:

1. When you call sum(10, 15), Python "jumps" to the function definition and assigns a = 10 and b =
15.
2. The function body then executes, adding a and b together, storing the result in c, and printing it.

Output:
Sum= 25

Multiple Calls:

You can call the same function multiple times with different arguments:

sum(10, 15) # Call with integers


sum(1.5, 10.75) # Call with floats

Output:

Sum= 25
Sum= 12.25

Here, the first call uses integers, and the second uses floating-point numbers. Notice how the function
handles different types of data without any change to its definition.

Understanding Arguments:

 Arguments are the values you pass to the function when you call it. In the example above, 10 and 15
are arguments.
 Dynamic Typing: The parameters a and b don’t know what type of data they will receive until the
function is called. At runtime, Python automatically assigns the type of data based on the arguments
passed:
o Integer arguments: When calling sum(10, 15), both a and b will be integers.
o Float arguments: When calling sum(1.5, 10.75), a and b will be floats.

This feature is known as dynamic typing, meaning the type of a variable is determined at runtime, not at
compile-time. This is different from languages like C or Java, where you must define the type of variables
beforehand.

Key Points:

1. Function Call: Functions are called by their name followed by parentheses.


2. Arguments: The values inside the parentheses are the arguments, passed to the function to process.
3. Dynamic Typing: Python determines the type of variables at runtime, so the same function can work
with different data types (like integers, floats, or strings).
4. Reusability: Once a function is defined, it can be used multiple times with different arguments,
making it reusable code.

Summary:

 A function runs only when called with specific arguments.


 You can call a function as many times as needed, with different arguments each time.
 Python uses dynamic typing, so the function can accept different types of data (like integers or
floats) without needing to change the function itself.
Returning Results from a Function in Python
When you define a function, you may want it to produce an output (the result of its execution) that can be used
elsewhere in the program. This is where the return statement comes in.

What is return in a Function?

The return statement is used to send a result back from the function to where the function was called. Once a
function hits a return statement, the function terminates, and the specified value is returned to the caller. This
value can then be assigned to a variable or used directly.

Here are some examples:

 Returning a simple value:

def add(x, y):


return x + y

The function add takes two arguments x and y, adds them together, and returns the result. If you call
add(5, 3), it will return 8.

 Returning multiple values: You can return multiple values from a function by separating them with commas.

def sum_and_diff(x, y):


return x + y, x - y

When you call sum_and_diff(5, 3), it returns both the sum (8) and the difference (2). You can capture
these values using multiple variables:

total, difference = sum_and_diff(5, 3)

 Returning a list or other data types: Functions can return collections like lists, dictionaries, tuples, or even
more complex data structures.

def create_list(x, y, z):


return [x, y, z]

The function create_list(1, 2, 3) will return the list [1, 2, 3].

Example: Using Return to Calculate the Sum

In Program 2, the sum function is modified to return the sum of two numbers rather than printing it:

def sum(a, b):


""" This function finds the sum of two numbers """
c = a + b
return c # returns result

# Call the function


x = sum(10, 15) # returns 25 and assigns to x
print('The sum is: ', x)

y = sum(1.5, 10.75) # returns 12.25 and assigns to y


print('The sum is: ', y)

Output:
The sum is: 25
The sum is: 12.25

 The function returns the sum of a and b and stores it in the variable x or y.
 The return statement allows us to use the result of the function in other parts of the program, like printing
or storing it in a variable.

Example: Even or Odd Check

In Program 3, we define a function even_odd that checks whether a number is even or odd:

def even_odd(num):
""" to know num is even or odd """
if num % 2 == 0:
print(num, "is even")
else:
print(num, "is odd")

# Call the function


even_odd(12) # Prints: 12 is even
even_odd(13) # Prints: 13 is odd

This function doesn't return any value. It simply prints whether the number is even or odd. This is an example of a
void function, meaning it performs an action but does not return anything.

Example: Factorial Function

In Program 4, the fact function calculates the factorial of a number by multiplying all the integers from n down to
1:

def fact(n):
""" to find factorial value """
prod = 1
while n >= 1:
prod *= n
n -= 1
return prod # return the calculated factorial

# Display factorials of first 10 numbers


for i in range(1, 11):
print(f'Factorial of {i} is {fact(i)}')

Output:

Factorial of 1 is 1
Factorial of 2 is 2
Factorial of 3 is 6
Factorial of 4 is 24
Factorial of 5 is 120
Factorial of 6 is 720
Factorial of 7 is 5040
Factorial of 8 is 40320
Factorial of 9 is 362880
Factorial of 10 is 3628800

The fact() function calculates the factorial of a given number n. For each number from 1 to 10, it returns the
factorial value.

Example: Prime Number Check

In Program 5, we define a function prime that checks whether a given number is prime or not:
def prime(n):
""" to check if n is prime or not """
x = 1 # this will be 0 if not prime
for i in range(2, n):
if n % i == 0: # if divisible by any number
x = 0
break
return x # returns 1 for prime, 0 for not prime

# Test if a number is prime or not


num = int(input('Enter a number: '))
result = prime(num)
if result == 1:
print(num, 'is prime')
else:
print(num, 'is not prime')

The prime() function returns 1 if the number is prime and 0 if it's not. The if statement checks the result and
prints accordingly.

Generating Prime Numbers

In Program 6, we use the prime() function to generate the first n prime numbers:

def prime(n):
""" to check if n is prime or not """
x = 1
for i in range(2, n):
if n % i == 0:
x = 0
break
return x

# Generate prime number series


num = int(input('How many primes do you want? '))
i = 2 # start with i value 2
c = 1 # count of primes
while True:
if prime(i): # if i is prime, display it
print(i)
c += 1 # increase counter
i += 1 # generate next number to test
if c > num: # if count exceeds num, break
break

Output (for 10 primes):

How many primes do you want? 10


2
3
5
7
11
13
17
19
23
29

The loop generates prime numbers by calling the prime() function until it finds the required number of primes. The
prime numbers are printed as they are found.

Key Concepts:
1. return Statement:
o Allows the function to send a result back to the caller.
o The result can be any data type (integer, float, list, etc.).
2. Void Functions:
o Functions that do not return anything (they may only perform actions like printing).
3. Dynamic Return Types:
o Python functions can return different types of values based on the input, making it flexible.

By using the return statement, you can make functions reusable and modular, which is a key feature in functional
programming.

Returning Multiple Values from a Function in


Python
In Python, a function is not restricted to returning just a single value like in languages such as C or Java. Instead,
Python functions can return multiple values simultaneously. These values are packed into a tuple, which is an
ordered, immutable collection of elements. This feature allows you to return multiple results from a function in a
straightforward manner.

Key Concepts

1. Returning Multiple Values: To return multiple values from a function, you can list them after the return
keyword, separated by commas. Python will automatically pack these values into a tuple.

Example:

return a, b, c

In this case, a, b, and c are returned as a tuple.

2. Tuple Unpacking: When calling the function, you can unpack the returned tuple into separate variables.

Example:

x, y, z = function()

Here, the returned tuple will be unpacked into the variables x, y, and z.

Practical Example 1: Returning Two Values

To demonstrate this concept, we can create a function sum_sub() that calculates the addition and subtraction of
two numbers, and returns both results.

Code:
# Function that returns two results
def sum_sub(a, b):
"""This function returns results of addition and subtraction of a, b."""
c = a + b
d = a - b
return c, d

# Get the results from sum_sub() function


x, y = sum_sub(10, 5)

# Display the results


print("Result of addition: ", x)
print("Result of subtraction: ", y)
Output:
Result of addition: 15
Result of subtraction: 5

 The function sum_sub() takes two parameters a and b.


 It calculates the sum (c = a + b) and the difference (d = a - b).
 These results are returned as a tuple (c, d).
 When the function is called with sum_sub(10, 5), it returns (15, 5), which is unpacked into the
variables x and y.
 The results are then printed.

Practical Example 2: Returning Multiple Values (Addition, Subtraction, Multiplication,


and Division)

Let's expand on this concept by creating a function that returns four values: addition, subtraction, multiplication, and
division.

Code:
# Function that returns multiple results
def sum_sub_mul_div(a, b):
"""This function returns results of addition, subtraction, multiplication, and
division of a, b."""
c = a + b
d = a - b
e = a * b
f = a / b
return c, d, e, f

# Get results from sum_sub_mul_div() function and store into t


t = sum_sub_mul_div(10, 5)

# Display the results using a for loop


print('The results are: ')
for i in t:
print(i, end=', ')
Output:
The results are:
15, 5, 50, 2.0,

 The function sum_sub_mul_div() takes two parameters a and b.


 It calculates four results:
o Addition (c = a + b)
o Subtraction (d = a - b)
o Multiplication (e = a * b)
o Division (f = a / b)
 These results are returned as a tuple (c, d, e, f).
 The tuple is assigned to the variable t when the function is called.
 The for loop iterates over the tuple and prints each result, separated by commas.

Key Points:

1. Tuple Packing: When multiple values are returned from a function, they are automatically packed into a
tuple.
Example: return a, b, c returns a tuple (a, b, c).

2. Tuple Unpacking: You can unpack the returned tuple into separate variables when calling the function. The
number of variables on the left side must match the number of values returned.

Example: x, y, z = function() unpacks the tuple into x, y, and z.

3. Multiple Values in Functions: You can return any number of values from a function. This feature makes it
easier to handle multiple results without the need for global variables or complex data structures.
4. Using Loops: When several values are returned, you can easily loop through the tuple to access each value
using a for loop or while loop.

Conclusion

In Python, functions can return multiple values, and these values are returned as a tuple. You can unpack the tuple
into separate variables when calling the function, which makes the code cleaner and more readable. This feature is
useful for functions that need to return multiple results, and Python's ability to return multiple values as tuples is
one of the language's strengths.

Functions are First Class Objects


✅ What Does "First-Class Objects" Mean?
In Python, functions are treated like any other variable or object. This means:

 You can assign a function to a variable.


 You can pass a function as an argument to another function.
 You can return a function from another function.
 You can store functions in data structures like lists, dictionaries, etc.

So, just like you can pass a number, string, or object, you can pass a function too!

✅ Program 9: Assign a function to a variable


def display(str):
return 'Hai ' + str

x = display("Krishna")
print(x)

💡 Explanation:

1. display("Krishna") is a function call. It returns "Hai Krishna".


2. That returned string is assigned to variable x.
3. print(x) simply prints the string value stored in x.

🧠 Output:
Hai Krishna
🔁 Alternate idea:

What if we assign the function itself to a variable?

def display(str):
return 'Hai ' + str

x = display # assigning function *not* calling it


print(x("Radha"))

Now, x behaves like the function display itself.

✅ Program 10: Function inside another function


def display(str):
def message():
return 'How are U?'
result = message() + str
return result

print(display("Krishna"))

💡 Explanation:

1. display() has a nested function message() inside it.


2. message() is not visible outside of display() – it is local to display().
3. When display("Krishna") is called:
o message() returns "How are U?"
o That is combined with "Krishna" → "How are U? Krishna"
4. display() returns the combined string.

🧠 Output:
How are U? Krishna

✅ Program 11: Passing a function as a parameter


def display(fun):
return 'Hai ' + fun

def message():
return 'How are U? '

print(display(message()))

💡 Explanation:

1. message() is called first → it returns "How are U? ".


2. display() is then called with that return value → 'Hai ' + 'How are U?'
3. So the final result is concatenation of those two strings.

🧠 Output:
Hai How are U?

🔁 What if we pass the function without calling it?


def display(fun):
return 'Hai ' + fun() # calling the function inside display()

def message():
return 'How are U? '

print(display(message)) # not using ()

💡 In this version:

 We pass the function message (without calling it) as an argument.


 Inside display, fun() is used to call it.

This is more powerful — lets us pass behavior, not just a value.

✅ Program 12: Returning a function from another function


def display():
def message():
return 'How are U?'
return message

fun = display()
print(fun())

💡 Explanation:

1. display() returns the function message, not its result.


2. fun = display() → Now fun is the same as message.
3. fun() calls the message() function.
4. Final output is "How are U?"

🧠 Output:
How are U?

🧠 Summary Table
Concept Code Example Behavior

Assign function result x = display("Krishna") x gets the result (string) of the function

Assign function itself x = display x becomes another name for the function

Nested Function def message() inside display() message is local to display

Function as Parameter display(message()) passes return value of message


Concept Code Example Behavior

Function passed directly display(message) passes function, not value

Return Function return message display returns the function itself

🎯 Real-Life Use Case:


You can use these techniques to build modular, dynamic programs, like:

 Creating function-based routing (e.g., web frameworks)


 Dynamic behaviour in AI apps
 Event handling (pass a function to handle user input)

Pass by Object Reference in Python


In programming languages like C and Java, when we pass values to functions, there are two commonly used
methods:

1. Pass by Value: A copy of the value is passed to the function, so any changes made to it within the function
do not affect the original value outside the function.
2. Pass by Reference: A reference (or memory address) of the variable is passed to the function, so any
changes made inside the function will affect the original value outside the function.

In Python, however, neither of these concepts strictly applies. Instead, Python uses a model called Pass by Object
Reference or Pass by Assignment. Let's break this down step by step:

Objects in Python

In Python, everything is treated as an object. This includes not only primitive types like integers, floats, and strings,
but also data structures like lists, tuples, and dictionaries. All these objects are stored in memory, and each object is
assigned an identity (memory address), which can be accessed using the id() function.

Example:
x = 10
print(id(x)) # Prints the memory address of the object 10

When we assign a value to a variable in Python, we're actually assigning a reference to an object in memory, not the
value itself. In the case above, x is a reference (or label) to the object 10 in memory. So, instead of imagining
variables as boxes with values, it's better to think of them as names attached to objects in memory.

Passing Objects to Functions


Immutable Objects (e.g., integers, strings, tuples)

When we pass immutable objects to a function, Python passes a reference to the object. However, since immutable
objects cannot be modified (their value cannot be changed), any changes within the function will create a new
object.

Let's see an example where we pass an integer to a function:

Example: Immutable Object - Integer


def modify(x):
x = 15 # Reassigning 'x' to a new object
print(x, id(x)) # Printing the modified value and its memory address

x = 10 # Original object
print(x, id(x)) # Original value and memory address
modify(x) # Passing 'x' to the function
print(x, id(x)) # Value and memory address after calling the function

Explanation:

 The integer 10 is stored in memory, and x references this object.


 When we call the function modify(x), we're passing the reference to the integer 10.
 Inside the function, we assign x = 15. Since integers are immutable, this creates a new object with value
15 and updates x to reference that new object.
 As a result, the memory address (id(x)) inside the function changes.
 The original x outside the function still references the integer 10, and its identity (memory address) remains
the same.

Output:
10 1617805016 # Original value and memory address
15 1617805096 # Modified value and new memory address inside the function
10 1617805016 # Original value and memory address after the function call

Since x is immutable (integers), any modification creates a new object, and the original x outside the function
remains unchanged.

Mutable Objects (e.g., lists, dictionaries)

Mutable objects, such as lists and dictionaries, behave differently. When passed to a function, changes made within
the function affect the same object because mutable objects can be modified in-place.

Let's look at an example where we pass a list to a function:

Example: Mutable Object - List


def modify(lst):
lst.append(9) # Modifying the list by appending a new element
print(lst, id(lst)) # Printing the modified list and its memory address

lst = [1, 2, 3, 4] # Original list


print(lst, id(lst)) # Original list and memory address
modify(lst) # Passing 'lst' to the function
print(lst, id(lst)) # Modified list and memory address after calling the function

Explanation:

 The list [1, 2, 3, 4] is an object in memory, and lst is a reference to this object.
 When we call the function modify(lst), we're passing the reference to the list.
 Inside the function, we modify the list by appending 9 to it. Since lists are mutable, the original list object is
modified, and the changes reflect outside the function as well.
 The memory address (id(lst)) of the list remains the same before and after the function call because it's
the same object being modified.

Output:
[1, 2, 3, 4] 37762552 # Original list and memory address
[1, 2, 3, 4, 9] 37762552 # Modified list and memory address inside the function
[1, 2, 3, 4, 9] 37762552 # Modified list and memory address after the function call

Since the list is mutable, the modifications to the list inside the function are reflected outside the function as well.

Creating a New Object Inside the Function

If you create a new object inside the function (even for mutable types), that object is local to the function. The
original object outside the function will not be affected.

Example: Creating a New List Inside the Function


def modify(lst):
lst = [10, 11, 12] # Creating a new list (a completely new object)
print(lst, id(lst)) # Printing the new list and its memory address

lst = [1, 2, 3, 4] # Original list


print(lst, id(lst)) # Original list and memory address
modify(lst) # Passing 'lst' to the function
print(lst, id(lst)) # Original list and memory address after calling the function
Explanation:

 The original list [1, 2, 3, 4] is passed to the function.


 Inside the function, we reassign lst to a new list [10, 11, 12]. This creates a new object.
 The memory address of the new list inside the function will be different from the original list.
 The original list outside the function remains unchanged because we created a new object inside the
function and reassigned lst to that new object.

Output:
[1, 2, 3, 4] 29505016 # Original list and memory address
[10, 11, 12] 29505976 # New list and its memory address inside the function
[1, 2, 3, 4] 29505016 # Original list and memory address after the function call

Conclusion

In Python, when you pass an object to a function, you're passing a reference to that object. The behavior of the
object (whether it gets modified or not) depends on whether the object is mutable or immutable:

 Immutable objects (e.g., integers, strings, tuples) cannot be modified. If you attempt to modify them,
Python creates a new object, and the changes are not reflected outside the function.
 Mutable objects (e.g., lists, dictionaries) can be modified in-place, and the changes will reflect outside the
function.

Formal and Actual Arguments in Functions


In Python (and other programming languages), functions are defined to perform a specific task. To make functions
flexible and reusable, they accept inputs (called arguments) that are passed during the function call. These
arguments can either be formal arguments (inside the function definition) or actual arguments (passed during the
function call).

1. Formal Arguments

 Definition: Formal arguments are the variables listed in the function definition. They act as placeholders to
receive values when the function is called.
 Example: In the function definition def sum(a, b):, a and b are formal arguments.
 Role: They are used inside the function to perform operations or computations using the passed values.

def sum(a, b): # a, b are formal arguments


c = a + b
print(c)
2. Actual Arguments

 Definition: Actual arguments are the values or variables passed to the function when it is called. These are
the real data or values that the function operates on.
 Example: In the function call sum(10, 15), the values 10 and 15 are actual arguments.
 Role: They are assigned to the formal arguments in the function definition when the function is executed.
 The actual arguments used in a function call are of 4 types:

o Positional arguments
o Keyword arguments
o Default arguments
o Variable length arguments
x = 10
y = 15
sum(x, y) # x, y are actual arguments

Positional arguments
Positional arguments are values passed to a function in the correct order. When you define a function that expects a
certain number of arguments in a specific order, you need to provide them in the same order when you call the
function.

Let's break it down with your example:

1. Function Definition:
def attach(s1, s2):
s3 = s1 + s2
print('Total string: ' + s3)

Here, the function attach expects two arguments: s1 and s2.

2. Function Call:

When calling the function, you need to pass two values (strings in this case) in the exact order that matches the
function definition.

attach('New', 'York')

 s1 will get 'New'.


 s2 will get 'York'.

So, inside the function, s3 will be 'New' + 'York', which results in 'NewYork'.

3. Output:
Total string: NewYork

What happens if you mix up the order or pass the wrong number of arguments?

 If you pass 'York' first and 'New' second:

attach('York', 'New')

The output will be:

Total string: YorkNew

 If you pass more or fewer arguments than expected (for example, 3 strings instead of 2):

attach('New', 'York', 'City')

This will give an error because the function is defined to accept only 2 arguments, but you've passed 3.
Summary:

Positional arguments must be passed in the correct order and number, exactly as expected in the function definition.

Keyword arguments
Keyword arguments allow you to pass arguments to a function by explicitly naming the parameters, rather than
relying on their position. This makes the code more readable and flexible, as you can pass the arguments in any
order.

Let's break it down with your example:

1. Function Definition:
def grocery(item, price):
print('Item = %s' % item)
print('Price = %.2f' % price)

Here, the function grocery expects two parameters:

 item (the name of the grocery item)


 price (the price of the item)

2. Function Call with Keyword Arguments:

You can call the function and specify which argument corresponds to which parameter by naming them:

grocery(item='Sugar', price=50.75)

 The item parameter will get the value 'Sugar'.


 The price parameter will get the value 50.75.

So the output will be:

Item = Sugar
Price = 50.75

3. Changing the Order:

With keyword arguments, the order doesn't matter. You can swap the positions of the arguments without causing
any issues:

grocery(price=88.00, item='Oil')

Even though price comes first and item comes second, the function still works correctly because the argument
names are used to match values to the correct parameters. The output will be:

Item = Oil
Price = 88.00

Summary:
 Keyword arguments allow you to specify which value goes into which parameter, regardless of their order in
the function call.
 This makes the function call more flexible and readable.

Default arguments
Default arguments allow you to set a default value for a function parameter, which is used
if the caller doesn't provide a value for that parameter. This can make your function calls
simpler by eliminating the need to always pass every argument.

Let's break it down with your example:

1. Function Definition:
def grocery(item, price=40.00):
print('Item = %s' % item)
print('Price = %.2f' % price)

 The function grocery has two parameters:


o item, which does not have a default value (it must always be provided when calling the function).
o price, which has a default value of 40.00. This means that if you don't provide a price when
calling the function, it will automatically use 40.00 as the price.

2. Function Call with Both Arguments:


grocery(item='Sugar', price=50.75)

 You pass both item ('Sugar') and price (50.75).


 The function uses the values you provide:

Item = Sugar
Price = 50.75

3. Function Call with Only One Argument:


grocery(item='Sugar')

 You only pass item ('Sugar') and do not pass a price.


 Since price has a default value of 40.00, the function will automatically use that value:

Item = Sugar
Price = 40.00

Summary:

 Default arguments let you define a parameter with a default value. If the caller doesn't provide a value for
that parameter, the default value is used.
 This is useful when you want to give the caller the option to skip providing certain values, and the function
will use sensible defaults in such cases.
Variable Length Arguments
In Python, you can write functions that can accept any number of arguments. This is useful when you don't know in
advance how many values will be passed to the function. Python provides two ways to handle this: Positional
Variable Length Arguments and Keyword Variable Length Arguments.

1. Positional Variable Length Arguments (*args)

When you use *args in the function definition, you can pass any number of positional arguments to the function,
and Python will store them as a tuple.

Example:
def add(farg, *args):
""" to add given numbers """
print('Formal argument= ', farg)
sum = 0
for i in args: # args is a tuple containing all extra arguments
sum += i
print('Sum of all numbers= ', (farg + sum))

# Function calls
add(5, 10) # Passes 1 extra argument
add(5, 10, 20, 30) # Passes 3 extra arguments

Output:
Formal argument= 5
Sum of all numbers= 15

Formal argument= 5
Sum of all numbers= 65

 In the first call, args will contain (10,), and the sum will be 10. The final result is 5 + 10 = 15.
 In the second call, args will contain (10, 20, 30), and the sum will be 60. The final result is 5 + 60 =
65.

2. Keyword Variable Length Arguments (**kwargs)

When you use **kwargs, you can pass any number of key-value pairs to the function, and Python will store them
as a dictionary.

Example:
def display(farg, **kwargs):
""" to display given values """
print('Formal argument= ', farg)
for x, y in kwargs.items(): # Iterating through the key-value pairs in kwargs
print('key = {}, value = {}'.format(x, y))

# Function calls
display(5, rno=10)
print()
display(5, rno=10, name='Prakash')

Output:
Formal argument= 5
key = rno, value = 10

Formal argument= 5
key = rno, value = 10
key = name, value = Prakash

 In the first call, kwargs will contain {'rno': 10}.


 In the second call, kwargs will contain {'rno': 10, 'name': 'Prakash'}.

Summary:

1. Positional Variable Length Arguments (*args): These are passed as a tuple. You can pass any number of
positional arguments.
o Example: *args collects all extra positional arguments.
2. Keyword Variable Length Arguments (**kwargs): These are passed as a dictionary. You can pass any
number of key-value pairs.
o Example: **kwargs collects all extra keyword arguments.

Both of these techniques allow you to write flexible functions that can handle varying numbers of inputs, making
your code more adaptable.

🌟 Local and Global Variables in Python


1. What is a Local Variable?
 Local Variable is a variable that you create inside a function.
 It lives only inside that function.
 Outside the function, it does not exist.
 When the function finishes running, the local variable is deleted from memory.

👉 Example:
def myfunction():
a = 1 # 'a' is created inside the function → Local variable
a += 1 # Increase 'a' by 1 → Now a = 2
print(a) # This will print 2

myfunction()
print(a) # ❌ This will cause an ERROR because 'a' is not available outside the
function

What happens here:

 myfunction() runs → it creates a = 1, adds 1, prints 2.


 After myfunction() is finished, a disappears.
 When we try print(a), Python says ➔ "NameError: name 'a' is not defined", because a no
longer exists.

2. What is a Global Variable?


 Global Variable is a variable that you create outside any function (at the top or anywhere outside).
 It is available everywhere in your code (in all functions written after it).

👉 Example:
a = 1 # 'a' is created outside → Global variable

def myfunction():
b = 2 # 'b' is a local variable inside the function
print('a =', a) # can access 'a' (global variable)
print('b =', b) # can access 'b' (local variable)

myfunction()

print(a) # ✅ 'a' is available here


print(b) # ❌ 'b' is NOT available here → ERROR!

What happens here:

 a = 1 is global ➔ it can be used anywhere.


 b = 2 is local to myfunction() ➔ can use b only inside myfunction().
 After the function ends, if we try print(b), Python says ➔ "NameError: name 'b' is not
defined".

🧠 In Short:
Feature Local Variable Global Variable
Where declared Inside a function Outside any function
Where available Only inside that function Everywhere (after it's created)
Life Duration Only while the function runs Throughout the program

🧠 Easy Memory Trick:


 Local = Limited ➔ (only inside function)
 Global = General ➔ (everywhere)

⚡ Quick Real-Life Example:


Imagine you are in a school:

 Local variable is like a student’s notebook ➔ only that student can use it inside their classroom
(function).
 Global variable is like a notice board ➔ everyone in school (all functions) can see it and use it.
🌟 The global Keyword and How to Use Global
Variables Inside a Function

1. What happens if Global and Local Variables have the Same


Name?
Suppose you have a variable a declared outside a function (global), and then you again declare a variable a
inside the function (local).

👉 Inside the function, Python will use the local a only, and ignore the global a.

Example:

a = 1 # global variable

def myfunction():
a = 2 # local variable (only inside function)
print('a =', a) # prints local 'a' (2)

myfunction()
print('a =', a) # prints global 'a' (1)

✅ Inside myfunction(), Python sees a local a = 2, so it prints 2.


✅ Outside, the global a = 1 is still safe.

Output:

a = 2
a = 1

2. How to Access and Modify Global Variable Inside a Function?


If you want the function to work with the global variable, you have to tell Python:

"Please, don't create a new local variable! I want to use the global one!"

You do this by writing ➔ global variable_name inside the function.

Example:

a = 1 # global variable

def myfunction():
global a # tell Python: 'use the global a'
print('global a =', a) # prints 1
a = 2 # changes the global 'a' to 2
print('modified a =', a) # prints 2

myfunction()
print('global a =', a) # prints 2 (it was changed inside the function)

Output:

global a = 1
modified a = 2
global a = 2

✅ Now, after calling the function, the global a is changed permanently to 2!

3. Problem when Names are Same


When local and global variables have the same name, it becomes confusing.
You might want to work with both — but normally you can't use both inside a function.

👉 If you use global, then only global will be available.


👉 If you don't use global, then only local will be available.

4. Solution: globals() Function


globals() is a special function in Python.
It gives a dictionary (like a table) of all global variables.

✅ Using it, you can pick any global variable inside your function even if a local one with the same name
exists!

Example:

a = 1 # global variable

def myfunction():
a = 2 # local variable
x = globals()['a'] # get global 'a' and store it in 'x'
print('global var a =', x) # prints 1
print('local var a =', a) # prints 2

myfunction()
print('global var a =', a) # prints 1

✅ Inside the function:

 a = 2 → local
 globals()['a'] → global value of a, which is 1

Output:

global var a = 1
local var a = 2
global var a = 1
🧠 In Short:
Feature Meaning
Same name for local & global Local is preferred inside function
Want to use global inside function Write global a
Want both local and global Use globals()['a']

⚡ Quick Real-Life Example:


Imagine you have:

 A personal notebook (local variable) on your desk.


 A school notice board (global variable) on the wall.

If you are in your classroom:

 If you write something on your notebook, it stays with you (local variable).
 If you want to see the school's notice board (global variable) while still using your notebook, you
use globals() to check the notice board without changing your notebook.

🎯 Final Tip:
 Use global keyword when you want to modify the global variable.
 Use globals()['varname'] when you want to read the global variable without losing the local
one.

🌟 Passing a Group of Elements to a Function

1. What is happening here?


Normally, when we pass something to a function, it could be a single number or a single string.
But sometimes, we want to pass many elements together — like many numbers or many names.

👉 To pass many elements together, we can put them inside a list, and then send the list to the function.

The function can then work on this list — like finding total, average, printing the names, etc.
🧠 Program 24: Passing a Group of Numbers to a
Function

🧠 Step 1: Take Numbers from the User

This line:

lst = [int(x) for x in input().split()]

means:

 input() → take input from keyboard. (example: "10 20 30")


 .split() → break the input wherever there is a space.
o So "10 20 30" becomes ["10", "20", "30"].
 int(x) for x in ... → convert each small piece (like "10") into an integer.
 [ ] → put all the numbers into a list.

✅ Now lst will be: [10, 20, 30]

🧠 Step 2: Pass the List to a Function

We send the whole list to a function:

x, y = calculate(lst)

 calculate(lst) will return two things — the total (sum) and the average.
 We store them into x and y.

🧠 Step 3: The calculate() Function


def calculate(lst):
n = len(lst) # count how many numbers are there
sum = 0
for i in lst:
sum += i # add all numbers to sum
avg = sum / n # find average
return sum, avg # send back sum and avg

 n = how many numbers are there (length of list).


 sum = total of all numbers.
 avg = sum divided by number of elements (average formula).
 Return both sum and avg back to the main program.

🎯 Full Example:
# define function
def calculate(lst):
""" to find total and average """
n = len(lst)
sum=0
for i in lst:
sum+=i
avg = sum/n
return sum, avg

# take numbers
print('Enter numbers separated by space: ')
lst = [int(x) for x in input().split()]

# call function
x, y = calculate(lst)

# display result
print('Total: ', x)
print('Average: ', y)

✅ Sample Output:

Enter numbers separated by space:


10 20 30 40 51
Total: 151
Average: 30.2

🎉 Program 25: Passing a Group of Strings to a


Function

Now, same idea, but with names/strings instead of numbers.

🧠 Step 1: Take Strings from the User


lst = [x for x in input().split(',')]

 input() → take input (example: "Gaurav,Vinod,Visal,Ankit")


 .split(',') → break wherever there is a comma ,
 So it becomes: ["Gaurav", "Vinod", "Visal", "Ankit"]
 [ ] → put into a list.

✅ Now lst will be: ["Gaurav", "Vinod", "Visal", "Ankit"]

🧠 Step 2: Pass the List to a Function

We send the list to a function:

display(lst)

 display() will print each string one by one.


🧠 Step 3: The display() Function
def display(lst):
""" to display the strings """
for i in lst:
print(i)

 For each item in the list, simply print it.

🎯 Full Example:
# define function
def display(lst):
""" to display the strings """
for i in lst:
print(i)

# take strings
print('Enter strings separated by comma: ')
lst = [x for x in input().split(',')]

# call function
display(lst)

✅ Sample Output:

Enter strings separated by comma:


Gaurav,Vinod,Visal,Ankit
Gaurav
Vinod
Visal
Ankit

🧠 In Short:
Concept Numbers Program Strings Program
Input Example 10 20 30 40 Gaurav,Vinod,Visal
How to split Using space Using comma
Function Name calculate() display()
Function Work Finds total and average Prints each string
What is Passed List of numbers [10,20,30,40] List of strings ["Gaurav","Vinod","Visal"]

⚡ Simple Life Example:


 Imagine you go to a fruit shop.
 You give a list of fruits ("apple, banana, mango") — the shopkeeper reads your list and gives you
fruits one by one.
 Similarly, you give a list of numbers ("10 20 30") — a machine calculates total cost and average
price.

🌟 Recursive Functions
 A recursive function is a function that calls itself again and again while solving a problem.
 It keeps calling itself until a simple case (called base case) is reached, and then it stops.

🌟 Example: Finding Factorial using Recursion


You know that:

 Factorial of 3 = 3 × 2 × 1 = 6
 Factorial of 5 = 5 × 4 × 3 × 2 × 1 = 120

Now, think of it like this:

 factorial(3) = 3 × factorial(2)
 factorial(2) = 2 × factorial(1)
 factorial(1) = 1 × factorial(0)

And we know that:


👉 factorial(0) = 1 (This is the base case — where the recursion stops.)

Now, step-by-step:

 factorial(0) = 1
 factorial(1) = 1 × 1 = 1
 factorial(2) = 2 × 1 = 2
 factorial(3) = 3 × 2 = 6

🎯 So, the final answer is 6!

🧠 Code for factorial:


def factorial(n):
if n == 0: # Base case: Stop when n is 0
return 1
else:
return n * factorial(n-1) # Recursive call

# Find factorials of numbers from 1 to 10


for i in range(1, 11):
print('Factorial of', i, 'is', factorial(i))
✅ Output:

Factorial of 1 is 1
Factorial of 2 is 2
Factorial of 3 is 6
Factorial of 4 is 24
Factorial of 5 is 120
Factorial of 6 is 720
Factorial of 7 is 5040
Factorial of 8 is 40320
Factorial of 9 is 362880
Factorial of 10 is 3628800

🌟 What is Towers of Hanoi?


 It’s an ancient puzzle game.
 You have 3 poles: A, B, C
 There are disks of different sizes stacked on pole A — bigger ones at the bottom.
 You have to move all disks from pole A to pole C, following two simple rules:
1. You can only move one disk at a time.
2. You cannot place a bigger disk on a smaller disk.

🌟 How to Solve Using Recursion?


When you have n disks:
 If n == 1:
👉 Just move that one disk directly from A to C.
 If n > 1:
1. Move n-1 disks from A to B (use C as temporary help).
2. Move the last (biggest) disk from A to C.
3. Move the n-1 disks from B to C (use A as help).

🌟 Code for Towers of Hanoi:


def towers(n, a, c, b):
if n == 1:
print('Move disk', n, 'from pole', a, 'to pole', c)
else:
towers(n-1, a, b, c) # Step 1
print('Move disk', n, 'from pole', a, 'to pole', c) # Step 2
towers(n-1, b, c, a) # Step 3

# take input
n = int(input('Enter number of disks: '))
towers(n, 'A', 'C', 'B')

✅ Output for 3 disks:

Move disk 1 from pole A to pole C


Move disk 2 from pole A to pole B
Move disk 1 from pole C to pole B
Move disk 3 from pole A to pole C
Move disk 1 from pole B to pole A
Move disk 2 from pole B to pole C
Move disk 1 from pole A to pole C

🌟 Why Recursion is Useful?


 Reduces code size: Complex problems can be solved in a few lines.
 Makes logic simple: Hard tasks become easier when broken into small parts.
 Perfect for problems that have repeated patterns like:
o Factorials
o Fibonacci numbers
o Tower of Hanoi
o Tree structures, etc.

🌟 In Short:
Concept Meaning
Recursive Function Function that calls itself
Base Case A simple condition where recursion stops
Factorial n × (n-1) × (n-2) × ... × 1
Concept Meaning
Towers of Hanoi Move disks from A to C following two rules using recursion

Anonymous Functions or Lambdas


1. What is an Anonymous Function?

 Normally, when we make a function in Python, we give it a name using def.


Example:

def square(x):
return x * x

 But sometimes, we want to create a very small and quick function without giving it a name.
That’s called an anonymous function.
 In Python, we make anonymous functions using a special word: lambda.
 That's why anonymous functions are also called lambda functions.

2. How to Write a Lambda Function?

Format:

lambda arguments : expression

Example:

lambda x: x*x

 Here, lambda tells Python we are making an anonymous function.


 x is the input.
 x*x is the work it will do (here, multiplying x by x, i.e., square).

✅ Important Point:
Lambda functions are used only for small, quick tasks.
They can have only one line of work (one expression).

3. How to use a Lambda Function?

You need to assign it to a variable (like a name), otherwise you can't call it later.

Example:

f = lambda x: x*x # Now 'f' is our function

Now you can use it:

value = f(5) # value will be 25


print('Square of 5 =', value)

🔵 Output:

Square of 5 = 25

4. More Examples of Lambda Functions

🔹 Sum of Two Numbers:

f = lambda x, y: x + y
result = f(1.55, 10)
print('Sum =', result)

Output:

Sum = 11.55

🔹 Find Bigger Number:

max = lambda x, y: x if x > y else y


a, b = [int(n) for n in input("Enter two numbers: ").split(',')]
print('Bigger number =', max(a, b))

If you type:

10, 25

You will get:

Bigger number = 25

5. Some Special Notes About Lambda:

 You can't write many lines inside a lambda function.


Only one simple expression is allowed.
 You don't write the return keyword — lambda automatically returns the result.
 Lambda functions are usually used when you need a quick, short function for small tasks.

6. Where are Lambda Functions Used?

Lambda functions are often used with other functions like:

 filter() – to filter data


 map() – to modify each element
 reduce() – to combine all elements into one value

These are used to process lists, strings, numbers quickly and easily.

Example (very basic):

numbers = [1, 2, 3, 4]
squares = list(map(lambda x: x*x, numbers))
print(squares)

Output:

[1, 4, 9, 16]

📢 Summary (In One Line):

Lambda functions are short, quick, unnamed functions you make using lambda keyword, mostly used for
small tasks and simple expressions.

🌟 Using Lambdas with filter() Function


 filter() is a built-in function in Python.
 It is used to filter (select) elements from a list, tuple, or string based on a condition.

Format:

filter(function, sequence)

 function → A function that checks something and returns True or False.


 sequence → A list (or tuple, or string) where each item will be tested.

✅ Only the elements for which the function returns True will be kept.
✅ Other elements will be ignored.

🧠 Example: Finding Even Numbers


Suppose we have a list:

[10, 23, 45, 46, 70, 99]

We want to pick only even numbers.

Step 1: Using Normal Function


First, let's define a normal function to check if a number is even:

def is_even(x):
if x % 2 == 0:
return True
else:
return False
 is_even(10) ➔ True
 is_even(23) ➔ False
 and so on...

Now use filter():

lst = [10, 23, 45, 46, 70, 99]

# Use filter to select only even numbers


lst1 = list(filter(is_even, lst))

print(lst1)

✅ Output:

[10, 46, 70]

🔵 What happens internally:

 10 → even → kept
 23 → odd → ignored
 45 → odd → ignored
 46 → even → kept
 70 → even → kept
 99 → odd → ignored

Step 2: Using a Lambda Function (Shorter Way)


Instead of writing a long is_even() function, we can directly use a lambda inside filter():

lst = [10, 23, 45, 46, 70, 99]

# Use lambda inside filter


lst1 = list(filter(lambda x: (x % 2 == 0), lst))

print(lst1)

✅ Output:

[10, 46, 70]

What's happening here?

 lambda x: (x % 2 == 0) is a tiny function that checks if x is even.


 filter() applies this function to every element in the list.

🎯 In Short:
Concept Meaning
filter(function, list) Selects items where function returns True
Normal Function A named function like def is_even(x): ...
Lambda Function A small quick function like lambda x: x%2==0
Result A list of filtered items

🎉 Quick Life Example


Imagine you have a basket of fruits 🍎🍌🍇🍍🍓:
You want to pick only apples 🍎.

 You check each fruit:


o If fruit is apple → keep it
o If not → ignore it

That's exactly what filter() does!

📢 Final Important Points:


 filter() gives a filter object → So we must use list() to convert it to a list.
 Lambda makes the code short and simple.
 Useful for quick filtering tasks in data processing.

🌟 Using Lambdas with map() Function


 map() is a built-in function in Python.
 It is used to apply a function to every item of a list (or tuple, or string).
 The function can change or modify each item.

Format:

map(function, sequence)

 function → A function that tells what to do with each item.


 sequence → A list (or tuple, etc.) of items.

✅ It returns the modified items.

🧠 Example: Finding Squares of Numbers


Suppose we have a list:

[1, 2, 3, 4, 5]

We want to square each number (multiply by itself).

Step 1: Using Normal Function


First, we write a function to find square:

def squares(x):
return x * x

 squares(2) ➔ 4
 squares(3) ➔ 9
 etc.

Now use map():

lst = [1, 2, 3, 4, 5]

# Apply squares() to each item using map


lst1 = list(map(squares, lst))

print(lst1)

✅ Output:

[1, 4, 9, 16, 25]

🔵 What happens inside:

 1×1→1
 2×2→4
 3×3→9
 4 × 4 → 16
 5 × 5 → 25

✅ Each number is squared.

Step 2: Using a Lambda Function (Shorter Way)


Instead of writing a separate function squares(x),
we can directly use a lambda inside map():

lst = [1, 2, 3, 4, 5]

# Use lambda to square each number


lst1 = list(map(lambda x: x*x, lst))
print(lst1)

✅ Output:

[1, 4, 9, 16, 25]

🧡 Lambda here is like a small instant function:


lambda x: x * x means "take x, multiply it by itself."

Step 3: Using map() on Two Lists


✅ Good News: map() can work with two or more lists also!
(BUT: the lists should be of the same length.)

Suppose:

lst1 = [1, 2, 3, 4, 5]
lst2 = [10, 20, 30, 40, 50]

We want to multiply corresponding elements:

 1 × 10 = 10
 2 × 20 = 40
 3 × 30 = 90
 etc.

We can do:

lst3 = list(map(lambda x, y: x*y, lst1, lst2))


print(lst3)

✅ Output:

[10, 40, 90, 160, 250]

🔵 Here:

 x takes elements from lst1


 y takes elements from lst2
 x * y means multiply them

✅ So:

 1×10 = 10
 2×20 = 40
 and so on.

🎯 In Short:
Concept Meaning

map(function, list) Apply a function to each item

Normal Function A named function like def squares(x): ...

Lambda Function A tiny quick function like lambda x: x*x

With two lists lambda x, y: x*y multiplies matching items

🎉 Quick Life Example


Imagine you are preparing sandwiches 🧡:
You have 5 breads and 5 fillings.

Using map(), you can combine bread and filling one-by-one automatically!

📢 Final Important Points:


 map() gives a map object → So we use list() to convert it into a list.
 Lambda functions make code shorter and faster.
 Useful for mass changes to a sequence (like squaring, doubling, multiplying, etc.).

Using Lambdas with reduce() Function


The reduce() function is used to combine the elements of a sequence (like a list) into a single value. It works by
repeatedly applying a function to the elements of the sequence, and at each step, it reduces the sequence until
there's just one value left.

Format of reduce() function:

reduce(function, sequence)

 function: A function that takes two arguments, processes them, and returns a new value (this function is
repeatedly applied to the elements).
 sequence: A list, tuple, or any iterable.

How does reduce() work?

Let’s look at an example where we want to calculate the product of all the numbers in a list:

lst = [1, 2, 3, 4, 5]
The goal is to multiply all these numbers together:

 First, we take the first two numbers: 1 and 2, and multiply them: 1 * 2 = 2
 Then, we take the result (2) and multiply it with the next number 3: 2 * 3 = 6
 Next, take 6 and multiply it with 4: 6 * 4 = 24
 Finally, take 24 and multiply it with 5: 24 * 5 = 120

So, the final result is 120, which is the product of all the numbers in the list.

This is how reduce() works!

Using reduce() with a Lambda Function

Instead of defining a function separately, we can use a lambda function inside reduce(). A lambda function is an
anonymous function that can be used for simple operations.

Let’s rewrite the same example using reduce() and a lambda function.

from functools import reduce # Import reduce from functools

# List of numbers
lst = [1, 2, 3, 4, 5]

# Using reduce with lambda function to calculate the product of all numbers
result = reduce(lambda x, y: x * y, lst)

# Print the result


print(result)

Explanation:

 lambda x, y: x * y: This is a lambda function that takes two numbers, x and y, and multiplies them.
 reduce(lambda x, y: x * y, lst): This applies the lambda function to the list lst. It will repeatedly
multiply the numbers in the list until only one result remains (the final product).

Output:

120

Another Example: Sum of Numbers from 1 to 50

Let’s say you want to calculate the sum of numbers from 1 to 50. You can use reduce() like this:

from functools import reduce # Import reduce from functools

# Using reduce with lambda function to calculate the sum of numbers from 1 to 50
sum_result = reduce(lambda a, b: a + b, range(1, 51))

# Print the result


print(sum_result)

Explanation:

 lambda a, b: a + b: This is a lambda function that takes two numbers, a and b, and adds them
together.
 range(1, 51): This creates a sequence of numbers from 1 to 50.
 reduce(lambda a, b: a + b, range(1, 51)): This applies the lambda function to the sequence,
repeatedly adding the numbers to get the total sum.

Output:

1275

Why use reduce()?

 reduce() is useful when you want to combine a sequence of items (like numbers, strings, etc.) into a single
value.
 It is particularly helpful when you want to perform operations like addition, multiplication, or concatenation
over a sequence, and reduce it to a single result.

Summary:

 reduce() takes a function and a sequence and reduces the sequence to a single value.
 It applies the function to the first two elements of the sequence, then uses the result with the next element,
and so on until the entire sequence is processed.
 It is often used with a lambda function to make the code more compact and elegant.
 Example tasks you can do with reduce():
o Calculate the product of a list of numbers.
o Calculate the sum of a range of numbers.
o Concatenate strings, and much more.

In Simple Words:

Imagine you have a list of things and you want to combine them into one result (like summing them up or
multiplying them). reduce() helps you do that by combining two things at a time until only one thing is left. 😊

Function Decorators
A decorator is a special function in Python that can be used to modify or add extra functionality to an existing
function without changing the original function's code.

You can think of it like adding extra features to an existing product. For example, if you have a basic car (your
function), a decorator would be like adding a sunroof, better seats, or air conditioning to it (extra functionality)
without changing the car's basic structure.

Steps to Create a Decorator

Here’s how decorators work, step-by-step:


Step 1: Define the Decorator Function

The first thing you do is define a function that will act as your decorator. This decorator function will take another
function as a parameter.

Example:

def decor(fun):
# fun is the function passed to the decorator

 Here, fun is the function we want to modify using the decorator. It will be passed into the decor function.

Step 2: Create a Function Inside the Decorator

Inside the decorator function, we define another function that actually modifies the behavior of the function passed
to the decorator. This inner function will do something to the result of the original function (e.g., modify or decorate
it).

Example:

def decor(fun):
def inner():
value = fun() # Call the function fun()
return value + 2 # Add 2 to the result of fun()
return inner # Return the inner function

 inner() is the function that will modify the result returned by fun(). It calls fun(), gets its result, and
adds 2 to it.
 Then, inner() is returned so it can be used in place of fun().

Step 3: Use the Decorator

Now, you can apply this decorator to any function that you want to modify. You do this by calling the decorator and
passing the function to it.

For example:

def num():
return 10

result_fun = decor(num) # Apply the decor() decorator to num() function


print(result_fun()) # Call the result (which is inner()) and print the output

 num() returns 10, but after applying the decor() decorator, the inner() function modifies it by adding 2.
So, the result is 12.

How it Works:

 The num() function simply returns 10.


 When we pass num() to decor(), it is wrapped by the inner() function inside decor().
 inner() calls num() (which gives 10), then adds 2 to it.
 Finally, the modified result 12 is returned.

Full Example:
# A decorator that adds 2 to the result of the function
def decor(fun):
def inner():
value = fun() # Call the original function
return value + 2 # Add 2 to the result
return inner # Return the modified function

# A function that returns 10


def num():
return 10

# Apply the decorator


result_fun = decor(num)

# Call the modified function and print the result


print(result_fun()) # Output will be 12

Output:

12

Why Use a Decorator?

 Code Reusability: You can use the same decorator on multiple functions without modifying their original
code.
 Separation of Concerns: You can keep the extra functionality (like adding 2, logging, or timing) separate
from the original logic of the function.

Alternative Way to Apply a Decorator

Instead of calling the decorator function manually, Python allows us to use the @ symbol to apply a decorator to a
function more neatly.

Example:

@decor
def num():
return 10

print(num()) # Output will be 12

This is equivalent to calling result_fun = decor(num) directly.

What’s Going On?

This program demonstrates decorators in Python, which are a way to modify or enhance the behavior of a function
without changing its original code.

Decorators are essentially functions that take another function as input and return a modified version of that
function.

Program 37: Basic Decorator

Let's start with Program 37, which shows a simple decorator that adds 2 to the result of a function.
Step-by-Step Breakdown:

1. Creating the Decorator (decor): The decor function is a decorator. A decorator is a function that takes
another function (fun) as an argument and returns a new function (inner) that modifies the behavior of
fun.

def decor(fun): # 'decor' is the decorator function


def inner(): # 'inner' is the function that does the modification
value = fun() # Call the 'fun' function (it will return 10 in this case)
return value + 2 # Add 2 to the result
return inner # Return the 'inner' function

o The decorator decor takes fun as input.


o Inside decor, we define the inner function, which calls fun() and adds 2 to the result.
o We return inner so that it can replace the original fun function.
2. Creating the Function to Decorate (num): Now, we have a simple function num() that returns 10. This is the
function we want to decorate using the decor decorator.

def num():
return 10

3. Applying the Decorator: To apply the decorator, we call decor(num) and assign the result to result_fun.
This replaces num with the decorated version, which adds 2 to the result.

result_fun = decor(num) # Applying the 'decor' decorator to 'num'


print(result_fun()) # Calling the decorated version of 'num'

When result_fun() is called, it executes the inner() function inside the decorator, which calls num()
(which returns 10), adds 2, and returns 12.

Output:

12

Program 38: Using @ Symbol to Apply Decorator

Instead of manually calling the decorator function (decor(num)), we can use the @ symbol to apply the decorator
more neatly.

Step-by-Step Breakdown:

1. Using @ to Apply Decorators: In Program 38, we apply the decor decorator to the num function using the
@decor syntax.

@decor # This applies the 'decor' decorator to 'num'


def num():
return 10

This is equivalent to:

def num():
return 10
num = decor(num) # Apply the decorator manually

2. Calling the Decorated Function: After applying the decorator with @decor, we can call num() directly. It will
automatically use the decorated version of the function.
print(num()) # Output will be 12

Output:

12

Program 39: Two Decorators

In Program 39, we introduce another decorator, decor1, which doubles the result of a function. Now we want to
apply both decor and decor1 to the num() function.

Step-by-Step Breakdown:

1. Creating the Second Decorator (decor1): The decor1 function is a second decorator that doubles the value
returned by the function.

def decor1(fun):
def inner():
value = fun() # Call the original function (e.g., num)
return value * 2 # Double the value
return inner

2. Applying Two Decorators: We apply decor1 first, then decor. This means the result will be modified twice:
o decor1 will double the result.
o decor will add 2 to the doubled value.

result_fun = decor(decor1(num)) # Apply both decorators


print(result_fun()) # Output will be 22

o num() returns 10.


o decor1(num) will double it to 20.
o decor(decor1(num)) will then add 2, resulting in 22.

Output:

22

Program 40: Using @ Symbol for Two Decorators

In Program 40, we use the @ symbol to apply both decorators (decor1 and decor) to the num() function. The
syntax is:

@decor
@decor1
def num():
return 10

This is equivalent to:

def num():
return 10

num = decor(decor1(num)) # Apply both decorators

When we call num(), it first gets modified by decor1 (doubling the value), and then by decor (adding 2).

print(num()) # Output will be 22


Output:

22

Summary of Decorator Syntax

 Multiple Decorators: When you apply multiple decorators, they are applied from bottom to top:
o @decor is applied first.
o @decor1 is applied second.

This is equivalent to:

def func():
pass
func = decor(decor1(func)) # Apply decorators in this order

Why Use Decorators?

 Separation of Concerns: You can separate the core logic of a function from its extra features (like modifying
its output).
 Code Reusability: You can reuse decorators for many functions without modifying them.
 Cleaner Code: Using the @ symbol makes the code look cleaner and easier to read.

Generators
Generators are special functions in Python that allow you to generate a sequence of values one at a time. The key
thing that makes generators unique is that they don’t return all values at once. Instead, they yield values one by
one. This is more memory efficient compared to returning all values in a list at once.

In a regular function, you use the return statement to return a value, but in a generator function, you use the
yield statement. When a function uses yield, it produces a generator object that will produce values on demand
when asked for them.

How Do Generators Work?

Generators are defined like normal functions, but instead of return, they use yield. Let's take an example.

Example 1: Generator for Numbers from x to y


def mygen(x, y): # Define a generator called 'mygen'
while x <= y: # As long as x is less than or equal to y
yield x # Yield the current value of x
x += 1 # Increment x by 1

 mygen is a generator function that takes two arguments: x and y.


 Inside the function, it runs a loop from x to y, and each time it yields the current value of x.
 The yield keyword returns the current value of x without exiting the function, allowing the function to
continue where it left off the next time it's called.
Using the Generator
g = mygen(5, 10) # Create a generator object from 5 to 10

for i in g: # Loop through the generator


print(i, end=' ') # Print each value yielded by the generator

Output:

5 6 7 8 9 10

 When you call mygen(5, 10), it returns a generator object.


 The for loop will automatically call next(g) on the generator, getting each number one by one: 5, 6, 7, 8,
9, 10.

Storing Generator Results in a List

You can convert a generator into a list if you want to store all its values at once. This is done using the list()
function.

lst = list(g) # Convert the generator 'g' into a list


print(lst) # Output: [5, 6, 7, 8, 9, 10]

Using next() with Generators

You can also manually retrieve values from a generator using the next() function. Each time you call next(), it
gives you the next value yielded by the generator.

g = mygen(5, 10) # Create the generator


print(next(g)) # Output: 5
print(next(g)) # Output: 6
print(next(g)) # Output: 7

 next(g) gives you the next number generated by g each time.


 Important: After all values are exhausted, calling next(g) will raise an error (StopIteration) because the
generator has no more values to yield.

Example 2: Simple Character Generator

Let's see another simple example where the generator returns characters.

def mygen():
yield 'A'
yield 'B'
yield 'C'

Here, mygen is a generator that yields 'A', 'B', and 'C'.

Using next() with Character Generator:


g = mygen() # Create the generator object
print(next(g)) # Output: A
print(next(g)) # Output: B
print(next(g)) # Output: C
print(next(g)) # Error: StopIteration

 The first call to next(g) returns 'A', the second returns 'B', and the third returns 'C'.
 After 'C' is yielded, calling next(g) will raise a StopIteration error because there are no more values in
the generator.

How yield Works

 yield: The yield statement temporarily suspends the function's execution, saving its state. When you call
next() on the generator again, the function picks up from where it left off, right after the last yield.
 This allows you to generate values on the fly, without storing them all in memory at once.

Advantages of Using Generators

1. Memory Efficiency: Generators don't store all the values in memory at once. Instead, they generate values
as needed. This is great when dealing with large datasets.
2. Lazy Evaluation: A generator only computes the next value when it is needed (on demand). This is called lazy
evaluation.
3. Cleaner Code: Generators can be a cleaner alternative to writing functions that return large lists or arrays.

Summary of Key Points:

 Generators are functions that use the yield statement to produce values one at a time, instead of
returning a whole list at once.
 next() is used to retrieve values from a generator.
 StopIteration is raised when the generator has no more values to yield.
 You can convert a generator to a list using the list() function if you need to store all its values.
 Advantages include memory efficiency and lazy evaluation, which can be particularly useful for handling
large data.

Structured Programming
Structured programming is a method used to solve complex problems by breaking them down into smaller,
manageable parts. These smaller parts are known as subtasks, and each subtask is solved using a function. This
approach makes the program easier to write, maintain, and understand.

The idea behind structured programming is to focus on dividing the main task into smaller functions, which can be
individually tested and reused. Once all the smaller functions are ready, you can combine them to solve the larger
problem.

Example Problem: Calculating the Net Salary of an Employee

Let’s say we need to calculate the net salary of an employee. The net salary is calculated by:

1. Gross Salary = Basic Salary + Dearness Allowance (DA) + House Rent Allowance (HRA)
2. Net Salary = Gross Salary - Provident Fund (PF) - Income Tax

Now, each of these components (DA, HRA, PF, and Income Tax) can be calculated using different formulas. Each of
these can be handled by a separate function. Once we have the values from these functions, we can easily calculate
the gross and net salary.
Steps of Structured Programming in This Example:

1. Break the task into smaller functions:


o Function to calculate DA (Dearness Allowance)
o Function to calculate HRA (House Rent Allowance)
o Function to calculate PF (Provident Fund)
o Function to calculate Income Tax
2. Use these functions in the main program to calculate the gross salary and then the net salary.

Breaking Down the Code

1. Functions to Calculate DA, HRA, PF, and Income Tax

Each of the functions takes the basic salary as input and returns a value that contributes to the final salary.

# Function to calculate DA (Dearness Allowance)


def da(basic):
""" DA is 80% of basic salary """
da = basic * 80 / 100
return da

 DA is calculated as 80% of the basic salary.


 The function takes basic salary as input and returns the DA amount.

# Function to calculate HRA (House Rent Allowance)


def hra(basic):
""" HRA is 15% of basic salary """
hra = basic * 15 / 100
return hra

 HRA is calculated as 15% of the basic salary.


 This function returns the HRA amount.

# Function to calculate PF (Provident Fund)


def pf(basic):
""" PF is 12% of basic salary """
pf = basic * 12 / 100
return pf

 PF is calculated as 12% of the basic salary.


 This function returns the PF amount.

# Function to calculate Income Tax


def itax(gross):
""" Tax is calculated at 10% on gross salary """
tax = gross * 0.1
return tax

 Income Tax is calculated as 10% of the gross salary.


 The function returns the tax amount.

2. Main Program to Calculate Gross and Net Salary

Once we have the functions for DA, HRA, PF, and tax, we can use them to calculate the gross salary and net salary of
the employee.

# Main program starts here

# Get the basic salary as input from the user


basic = float(input('Enter basic salary: '))

 The program asks the user to enter the basic salary.

# Calculate the gross salary


gross = basic + da(basic) + hra(basic)
print('Your gross salary: {:10.2f}'.format(gross))

 The gross salary is calculated by adding:


o Basic Salary
o DA (calculated using da(basic))
o HRA (calculated using hra(basic))
 The result is displayed with two decimal points using format().

# Calculate the net salary


net = gross - pf(basic) - itax(gross)
print('Your net salary: {:10.2f}'.format(net))

 The net salary is calculated by subtracting:


o PF (calculated using pf(basic))
o Income Tax (calculated using itax(gross))
 The result is displayed with two decimal points using format().

Example Output

Let’s say the basic salary entered by the user is 20,000.

Enter basic salary: 20000


Your gross salary: 39000.00
Your net salary: 32700.00

Here’s how the calculations work:

1. DA = 80% of 20,000 = 16,000


2. HRA = 15% of 20,000 = 3,000
3. Gross Salary = 20,000 (Basic) + 16,000 (DA) + 3,000 (HRA) = 39,000
4. PF = 12% of 20,000 = 2,400
5. Income Tax = 10% of 39,000 = 3,900
6. Net Salary = 39,000 (Gross) - 2,400 (PF) - 3,900 (Tax) = 32,700
Why Use Structured Programming?

 Clarity: By dividing a big problem into smaller tasks (functions), the code is easier to read and understand.
 Reusability: Functions can be reused wherever necessary without rewriting the same code.
 Maintainability: If you need to make changes, you only need to modify the relevant function without
affecting the whole program.
 Debugging: It is easier to debug and test small, independent functions rather than a large, monolithic
program.

Summary

 Structured Programming helps in breaking a large task into smaller, manageable pieces called functions.
 Each function solves a subtask, and these functions are then used to solve the overall problem.
 In this example, we divided the salary calculation problem into four tasks (DA, HRA, PF, and Income Tax),
each handled by its own function.
 The program uses these functions to calculate the gross and net salary of an employee.

Creating our Own Modules in Python


What is a Python Module?

In Python, a module is a file that contains Python code, such as functions, variables, and classes, that can be used in
other Python programs. Modules are created to organize code into smaller, reusable parts. Instead of writing the
same code repeatedly in different programs, you can import a module and reuse the code it contains.

Why Use Modules?

1. Code Reusability: You can write a function or class once and reuse it in multiple programs.
2. Organization: Modules help in organizing related functions and classes into one file, making the code more
manageable.
3. Collaboration: In a team project, different programmers can work on different modules. Once a module is
created, others can use it.

Creating Your Own Module

In Python, you can create your own module by simply writing the code you want to reuse in a file and giving it a
name (ending with .py). This file becomes your module.

For example, let’s create a module called employee.py that contains several functions to calculate different parts
of an employee's salary:

Functions in the employee.py Module

Here’s the code inside employee.py that calculates various salary-related components:
# Save this code as employee.py

# Function to calculate Dearness Allowance (DA)


def da(basic):
"""DA is 80% of the basic salary."""
da = basic * 80 / 100
return da

# Function to calculate House Rent Allowance (HRA)


def hra(basic):
"""HRA is 15% of the basic salary."""
hra = basic * 15 / 100
return hra

# Function to calculate Provident Fund (PF)


def pf(basic):
"""PF is 12% of the basic salary."""
pf = basic * 12 / 100
return pf

# Function to calculate Income Tax (Tax)


def itax(gross):
"""Tax is calculated at 10% on the gross salary."""
tax = gross * 0.1
return tax

Explanation of the employee.py Module:

 da(basic): Calculates Dearness Allowance (DA) as 80% of the basic salary.


 hra(basic): Calculates House Rent Allowance (HRA) as 15% of the basic salary.
 pf(basic): Calculates Provident Fund (PF) as 12% of the basic salary.
 itax(gross): Calculates Income Tax (IT) as 10% of the gross salary.

Each of these functions takes either the basic salary or gross salary as input and returns the calculated value.

Using the Module in Another Program

Once the module is created, you can use it in other Python programs by importing it.

Importing the Module

You can import the employee module into your program using one of two ways:

1. Standard Import (using the module name before the function):

import employee
employee.da(basic)
employee.hra(basic)
employee.pf(basic)
employee.itax(gross)

2. From-import (importing everything from the module so you don’t need to use the module name):

from employee import *


da(basic)
hra(basic)
pf(basic)
itax(gross)
The second method is more convenient because you don’t need to type the module name (employee) every time
you call a function.

Example Program Using the employee Module

Now, let’s write a program that calculates the gross salary and net salary using the functions from the employee
module.

# Program using the employee module to calculate gross and net salaries

from employee import * # Import all functions from employee module

# Get the basic salary from the user


basic = float(input('Enter basic salary: '))

# Calculate gross salary


gross = basic + da(basic) + hra(basic) # Basic + DA + HRA
print('Your gross salary: {:10.2f}'.format(gross))

# Calculate net salary


net = gross - pf(basic) - itax(gross) # Gross - PF - Income Tax
print('Your net salary: {:10.2f}'.format(net))

What Happens in the Program?

1. User Input: The program asks the user to enter the basic salary.
2. Gross Salary Calculation: It calculates the gross salary by adding:
o Basic salary
o DA (calculated using da(basic))
o HRA (calculated using hra(basic))
3. Net Salary Calculation: It calculates the net salary by subtracting:
o PF (calculated using pf(basic))
o Income Tax (calculated using itax(gross))

Example Output

If the user enters a basic salary of 15,000, the program will display:

Enter basic salary: 15000


Your gross salary: 29250.00
Your net salary: 24525.00
Here’s how the calculation works:

1. DA = 80% of 15,000 = 12,000


2. HRA = 15% of 15,000 = 2,250
3. Gross Salary = 15,000 (Basic) + 12,000 (DA) + 2,250 (HRA) = 29,250
4. PF = 12% of 15,000 = 1,800
5. Income Tax = 10% of 29,250 = 2,925
6. Net Salary = 29,250 (Gross) - 1,800 (PF) - 2,925 (Tax) = 24,525

Why Use Modules?

1. Reusability: Once you create a module, you can reuse it in multiple programs without rewriting the same
code.
2. Organization: By grouping related functions into a module, the code becomes more organized and easier to
manage.
3. Collaboration: In a team, different members can work on different modules, and each person can easily
import and use the modules developed by others.

Summary

 Module: A file containing Python functions, classes, and variables.


 Creating a Module: You create a module by saving Python code in a .py file.
 Importing a Module: You can import a module using the import or from-import statement to use its
functions in your program.
 Advantages: Modules make code reusable, organized, and easier to maintain.

The Special Variable __name__


What is the __name__ Variable in Python?

In Python, the special variable __name__ is automatically set by the Python interpreter when a program or module
is run. It helps us understand whether a Python script is being executed directly as the main program or if it's being
imported as a module into another program.

How Does the __name__ Variable Work?

1. When you run a Python program directly (e.g., by typing python program.py in the terminal), Python
sets the value of __name__ to '__main__'.
2. When you import the Python file as a module into another program (e.g., import program), Python sets
the value of __name__ to the name of the module (i.e., the name of the Python file without the .py
extension).

By checking the value of __name__, we can determine how the Python code is being executed.

Example 1: Running the Program Directly

Let’s look at the first example where we write a Python program one.py that uses __name__:

# Save this as one.py

def display():
print('Hello Python!')

if __name__ == '__main__':
display() # This will call the display function if run directly
print('This code is run as a program')
else:
print('This code is run as a module')

What Happens When You Run one.py Directly?

1. When you run one.py directly (using python one.py), the Python interpreter will set __name__ to
'__main__' because the file is being executed as the main program.
2. Inside the script, there is a check:

if __name__ == '__main__':

Since __name__ is '__main__', the code inside the if block will be executed, which includes:

o Calling the display() function, which prints Hello Python!.


o Printing This code is run as a program.

Output:
Hello Python!
This code is run as a program

Example 2: Importing the Program as a Module

Now let’s look at another Python program two.py that imports one.py as a module:

# Save this as two.py

import one # Importing one.py as a module


one.display() # Calling the display() function from the one module

What Happens When You Run two.py?

1. When you run two.py (using python two.py), Python first imports the one module.
2. During the import, Python sets the value of __name__ in the one.py script to 'one' because one.py is
now being treated as a module named one, not as the main program.
3. The else block in one.py is executed because __name__ is no longer '__main__'. This means the code in
the else block will print:
o This code is run as a module.
4. After the import, two.py calls one.display(), which prints:
o Hello Python! (because display() is called from one.py).

Output:
This code is run as a module
Hello Python!

Key Points:

 Direct Execution: When you run a Python file directly, __name__ is set to '__main__'.
 Module Import: When you import a Python file as a module, __name__ is set to the module's name (the
filename without .py).
 Conditional Code Execution: By using the condition if __name__ == '__main__', you can ensure that
certain parts of the code only run when the file is executed directly, not when it's imported as a module.

Why is This Useful?

1. Code Reusability: By using __name__, you can write code that behaves differently depending on whether it
is being used directly or as part of another program.
2. Testing and Debugging: If you want to test some functions in a file without running them when importing
the file as a module, you can wrap that testing code inside if __name__ == '__main__'. This ensures
the test code runs only when the file is executed directly.
3. Modular Design: It helps you design your Python code in a modular way. The functions and classes can be
imported and used by other programs, while certain parts (e.g., testing code) can be kept separate and only
run when needed.

Summary:

 The __name__ variable in Python helps you determine how a program is executed: directly or as a module.
 When you run a script directly, __name__ is set to '__main__'. When you import it as a module,
__name__ is set to the name of the module.
 Using if __name__ == '__main__', you can write code that only runs when the script is executed
directly, making your code modular and reusable.

Key Points to Remember About Functions in Python


1. What is a Function?
o A function is a block of code that performs a specific task. It is similar to a program but can be
reused multiple times in different parts of your program.
2. Function Definition:
o A function is defined using the def keyword followed by the function name and parameters (if any).

def function_name(parameter1, parameter2, ...):


# function body

3. Function Execution:
o A function is executed only when it is called using its name.

function_name(arguments)

4. The return Statement:


o The return statement is used to return a value from the function. A function can return one or
more values, or none.

def add(a, b):


return a + b
result = add(3, 5)

5. Function Argument Passing (Pass by Object Reference):


o In Python, arguments are passed to functions by object reference. This means that when you pass a
mutable object (like a list) to a function, changes to that object will affect the original object outside
the function.
6. Types of Arguments:
o Formal Arguments: These are the parameters defined in the function signature.

def greet(name): # name is a formal argument


print("Hello, " + name)

o Actual Arguments: These are the values passed to the function when calling it.

greet("Alice") # "Alice" is the actual argument

7. Positional Arguments:
o These are arguments passed to the function in the correct position and number. The first argument
goes to the first parameter, the second to the second, and so on.

def subtract(a, b):


return a - b
result = subtract(10, 5) # 10 and 5 are positional arguments

8. Keyword Arguments:
o These are arguments where you specify the parameter name along with the value.

def display(name, age):


print(name, age)
display(age=25, name="Bob") # Using keyword arguments

9. Default Arguments:
o If an argument is not provided in the function call, it takes a default value as specified in the
function definition.

def greet(name, message="Hello"):


print(message + " " + name)
greet("Alice") # message will take the default value "Hello"

10. Variable-Length Arguments:


o *args: Used to pass a variable number of positional arguments to a function.

def sum_numbers(*args):
return sum(args)
print(sum_numbers(1, 2, 3)) # 6

o **kwargs: Used to pass a variable number of keyword arguments (key-value pairs).

def print_info(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_info(name="Alice", age=25)

11. Local and Global Variables:


o Local Variables: These are variables declared inside a function and are accessible only within that
function.
o Global Variables: These are variables declared outside of any function and can be accessed from any
part of the program, including inside functions.
12. Recursive Functions:
o A function that calls itself is known as a recursive function. It is useful for problems that can be
broken down into smaller, similar problems (e.g., factorial, Fibonacci sequence).

def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)

13. Anonymous Functions (Lambda Functions):


o A function without a name is called a lambda function. It is used to create small, one-line functions.

square = lambda x: x * x
print(square(5)) # Output: 25

14. Using Lambda with Functions:


o filter(): Used to filter elements from a sequence based on a function.
numbers = [1, 2, 3, 4, 5]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers)) # [2, 4]

o map(): Applies a function to each item in a sequence.

numbers = [1, 2, 3, 4]
squared_numbers = map(lambda x: x ** 2, numbers)
print(list(squared_numbers)) # [1, 4, 9, 16]

o reduce(): Reduces a sequence to a single value by applying a function cumulatively.

from functools import reduce


numbers = [1, 2, 3, 4]
product = reduce(lambda x, y: x * y, numbers)
print(product) # 24

15. Modules in Python:


o A module is a file that contains Python definitions and statements, such as functions, classes, and
variables. Python provides many built-in modules, and you can create your own modules to organize
your code.

import math
print(math.sqrt(16)) # Output: 4.0

16. Decorators:
o A decorator is a function that takes another function as an argument and returns a new function. It
is often used to modify the behavior of functions.

def decorator(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper

@decorator
def greet():
print("Hello!")
greet() # Output: Before function call \n Hello! \n After function call

17. Generators:
o A generator is a special type of function that returns a sequence of values using the yield
statement. Unlike regular functions that return a single value, a generator returns an iterator that
produces values one at a time.

def count_up_to(max):
count = 1
while count <= max:
yield count
count += 1

counter = count_up_to(5)
for number in counter:
print(number) # Output: 1 2 3 4 5

18. The __name__ Variable:


o The special variable __name__ tells you whether a Python program is being run directly or imported
as a module. If __name__ == '__main__', the program is being run directly as a script. If
__name__ contains the name of the module, it means the program is imported.
if __name__ == '__main__':
print("This is running as a script")
else:
print("This is being imported as a module")

Exceptions
Errors in a Python Program
In general, we can classify errors in a program into one of these three types:
Compile-time errors
Runtime errors
Logical errors

Compile-Time Errors in Python (Syntax Errors)


Compile-time errors, also known as syntax errors, occur when Python detects a problem in the structure of the
code. These errors prevent the program from being executed. The Python interpreter cannot proceed with the
program because the syntax is incorrect. These errors are caught before the program runs, during the compilation
phase.

Common Causes of Compile-Time Errors:

1. Forgetting to use necessary punctuation (like a colon):


o In Python, certain statements like if, for, while, and def require a colon (:) at the end of the
statement to indicate the start of a block of code. Forgetting the colon will cause a syntax error.
2. Indentation Issues:
o Python uses indentation to define blocks of code. If the indentation is inconsistent (such as mixing
spaces and tabs or using different numbers of spaces), Python will throw an IndentationError.

Example 1: Missing Colon

In this example, a colon is missing after the if statement, which causes a SyntaxError.

x = 1
if x == 1 # Missing colon here
print('Where is the colon?')

Output:

File "ex.py", line 3


if x == 1
^
SyntaxError: invalid syntax

 Explanation: The Python interpreter expects a colon after the condition x == 1 because it marks the start
of the code block that will execute if the condition is true. Since the colon is missing, Python cannot
understand the code, so it raises a SyntaxError.
Example 2: Indentation Error

In this example, the indentation is incorrect because the second print statement inside the if block is not aligned
properly.

x = 10
if x % 2 == 0:
print(x, 'is divisible by 2')
print(x, 'is even number') # Incorrect indentation

Output:

File "ex.py", line 5


print(x,' is even number')
^
IndentationError: unexpected indent

Explanation:

 IndentationError: Python expects all statements within the same block to be indented equally. Here, the
second print statement is indented incorrectly, causing an IndentationError. All the statements inside the
if block should have the same number of spaces or tabs.

How Python Helps You Fix Compile-Time Errors:

When Python encounters a syntax error or indentation error, it provides the following helpful information:

 Line number: The error message will point to the line where the error occurred, making it easier to find and
fix.
 Error description: Python will describe the type of error (like SyntaxError or IndentationError) and
sometimes provide hints (like "unexpected indent").

How to Fix Compile-Time Errors:

1. Check for missing colons: After if, for, while, def, and similar statements, ensure you add a colon (:) at
the end.
o Example:

if x == 1:
print('Where is the colon?')

2. Fix indentation: Make sure that all statements within the same block have the same indentation level
(usually 4 spaces). Avoid mixing spaces and tabs.
o Example:

if x % 2 == 0:
print(x, 'is divisible by 2')
print(x, 'is even number') # Correct indentation

Summary:

 Compile-time errors happen due to syntax mistakes (like missing colons or incorrect indentation).
 Python will give a helpful error message with the line number and a description of the error, so you can
easily find and fix the problem.
 Always check for missing colons and ensure that your indentation is consistent to avoid these errors.
Runtime Errors in Python
Runtime errors occur when a program runs, but something goes wrong while the program is executing. Unlike
compile-time errors (which are detected before the program runs), runtime errors are only detected when the
program is actually running. These errors happen when the Python Virtual Machine (PVM) tries to execute the
program but encounters a problem.

Key Points about Runtime Errors:

 Detected during execution: Python can't catch these errors when compiling the code, but it catches them
when the program is actually running.
 Examples: Common runtime errors include trying to divide by zero, accessing a list element that doesn't
exist, or using incompatible data types in an operation.

Let's break down two examples to understand runtime errors better.

Example 1: TypeError (Adding incompatible types)

In this example, the function concat tries to add a string and an integer together. This results in a TypeError.

def concat(a, b):


print(a + b)

# Call the function and pass a string and a number


concat('Hai', 25)

Output:

Traceback (most recent call last):


File "ex.py", line 6, in <module>
concat('Hai', 25)
File "ex.py", line 3, in concat
print(a + b)
TypeError: Can't convert 'int' object to str implicitly.

Explanation:

 The function concat is designed to add two values, but the way it adds depends on the types of values
passed:
o If both values are strings, they are concatenated (joined).
o If both values are numbers, they are added (summed).
 But here, the function is called with a string ('Hai') and an integer (25). Python can't automatically decide
how to handle this because it doesn't know how to combine a string with a number. This results in a
TypeError.

Runtime Error Behavior:

 Python doesn’t check the types during compilation. It only checks them when the program is running.
When Python tries to add a string and an integer, it realizes they are incompatible types and raises a
TypeError.
 The error message helps by saying: "Can't convert 'int' object to str implicitly." This tells us that Python
can't automatically convert the integer to a string.

Example 2: IndexError (Accessing an invalid index in a list)

In this example, we try to access an index that doesn't exist in a list, which raises an IndexError.
animal = ['Dog', 'Cat', 'Horse', 'Donkey']
print(animal[4])

Output:

Traceback (most recent call last):


File "ex.py", line 3, in <module>
print(animal[4])
IndexError: list index out of range

Explanation:

 The list animal contains 4 elements: ['Dog', 'Cat', 'Horse', 'Donkey'].


 In Python, list indices start from 0. So, the indices for this list are:
o animal[0] → 'Dog'
o animal[1] → 'Cat'
o animal[2] → 'Horse'
o animal[3] → 'Donkey'
 But the code tries to access animal[4], which does not exist (the list only has indices 0 to 3). This results in
an IndexError because the index 4 is out of range.

Runtime Error Behavior:

 The Python interpreter detects that the index is invalid only when trying to access it at runtime.
 The error message tells us: "IndexError: list index out of range". This means we tried to access an index that
doesn't exist in the list.

How to Handle Runtime Errors:

When a runtime error occurs, Python will display the line number and type of error, which can help you fix the
problem.

Common Solutions:

1. TypeError Solution: Ensure that the data types you are working with are compatible. For example, if you
want to concatenate a string and a number, you should convert the number to a string first:

def concat(a, b):


print(a + str(b)) # Convert b to a string before adding

concat('Hai', 25)

2. IndexError Solution: Check if the index is valid before trying to access the list element. You can use len() to
check the length of the list before accessing an index:

animal = ['Dog', 'Cat', 'Horse', 'Donkey']


index = 4
if index < len(animal): # Check if index is within the list range
print(animal[index])
else:
print("Index is out of range!")

Some Runtime Errors Can't Be Easily Fixed:

 In some cases, runtime errors are due to conditions that can't be easily prevented (like running out of
memory or trying to access a resource that isn't available). In such cases, exception handling is used to
manage these errors gracefully.
Conclusion:

 Runtime errors happen while the program is running, and they can be due to invalid operations (like adding
incompatible types or accessing non-existent list indices).
 Python displays helpful error messages (with line numbers) to help you fix these errors.
 You can prevent most runtime errors by ensuring that your data types and index values are valid and by
using exception handling when necessary.

Logical Errors in Python


Logical errors are errors in the logic or design of a program. These errors do not show up during compilation or
runtime, so they are hard to spot unless you test the program and check the output. Essentially, the program runs,
but it gives the wrong output because the logic behind the code is flawed.

These errors are not detected by the Python compiler or interpreter, meaning that the program will run without
any obvious signs of trouble, but the result will be wrong. It is the programmer's responsibility to find and fix these
logical errors.

Example 1: Logical Error in Calculating Salary

Let’s look at a program where the programmer wants to increment an employee's salary by 15%. But due to a
logical error, the calculation is incorrect.

def increment(sal):
sal = sal * 15 / 100 # This is the wrong formula.
return sal

# Calling the function and passing the salary


sal = increment(5000.00)
print('Incremented salary= %.2f' % sal)

Output:

Incremented salary= 750.00

Explanation:

 The goal is to increase the salary by 15%, but the formula used is sal = sal * 15 / 100. This only
calculates the increment (i.e., 15% of the salary) but does not add it to the original salary.
 If you manually calculate 15% of 5000, you get 750. But the correct formula to increase the salary by 15%
should be:

sal = sal + sal * 15 / 100

This adds the increment to the original salary.

Corrected Output:

Incremented salary= 5750.00

So, this is a logical error because the logic behind the calculation was wrong, even though the program ran without
any errors.
Example 2: Runtime Errors and Exception Handling

Next, let's look at a program where we perform a division and write the result to a file. The program works fine
when there is no problem, but if the user enters zero for the denominator, it causes a runtime error called
ZeroDivisionError.

# an exception example

# Open a file to write the result


f = open("myfile", "w")

# Accept two numbers from the user


a, b = [int(x) for x in input("Enter two numbers: ").split()]

# Perform division
c = a / b

# Write the result into the file


f.write("writing %d into myfile" % c)

# Close the file


f.close()

print('File closed')

Expected Output (if division works fine):

Enter two numbers: 10 2


File closed

Now, what happens if the user enters 10 and 0 for a and b?

Output:

Enter two numbers: 10 0


Traceback (most recent call last):
File "ex.py", line 8, in <module>
c = a / b
ZeroDivisionError: division by zero

Explanation:

 In this case, when you divide 10 / 0, it raises a ZeroDivisionError because dividing by zero is
mathematically impossible.
 When the error occurs, the program stops immediately, and the subsequent lines of code (like closing the
file) are not executed.
 This is problematic because the file was never closed properly, which could lead to data loss or a corrupted
file.

Why Should Errors Be Handled?

If the error is not handled properly, it can cause serious issues:

1. Data Loss: In the above example, if the file isn't closed properly, any data that was written to the file might
be lost.
2. Corrupted Software: If errors are not caught and handled, the program may stop working completely.
3. User Experience: If the program abruptly crashes, users may lose trust in the software.

Solution: Exception Handling


To avoid such problems, you can use exception handling in Python. This is a way to catch errors before they cause
the program to crash.

Here is how you can handle the ZeroDivisionError in the program using a try-except block:

try:
# Perform division
c = a / b
# Open the file and write the result
f = open("myfile", "w")
f.write("writing %d into myfile" % c)
except ZeroDivisionError:
print("Error: Division by zero is not allowed!")
finally:
# Ensure the file is closed whether there's an error or not
f.close()
print('File closed')

Explanation of try, except, and finally:

 try: This block contains the code that might cause an error. In this case, it’s the division operation.
 except: If an error occurs in the try block, Python will jump to the except block. In this case, it catches the
ZeroDivisionError and prints a message.
 finally: This block will always execute, regardless of whether an error occurred or not. Here, it ensures
that the file is closed properly.

Conclusion

 Logical errors are problems with the design or logic of your program. They do not cause the program to
crash, but they produce the wrong output. These are the programmer’s responsibility to find and fix.
 Runtime errors happen during the program’s execution, such as dividing by zero or accessing invalid data.
These errors need to be handled using exception handling techniques like try-except blocks.
 Exception handling ensures that the program doesn’t crash unexpectedly, which improves reliability and
helps preserve data integrity.

Exception
What is an Exception?

An exception is an error that happens while the program is running (during runtime). The difference between a
regular error and an exception is that an exception is an error that the programmer can handle or fix, while a regular
error is something that might be too difficult or impossible to fix within the program itself.

For example, when you're trying to divide a number by zero, that's an exception because the programmer can
handle this case and prevent the program from crashing. However, if you're trying to open a file that doesn't exist,
that's usually an error because the program can't do much to fix it automatically.

How are Exceptions Handled?

Python allows programmers to handle exceptions using exception handling mechanisms like try, except, and
finally blocks. When a potential exception occurs, the program can catch it using except and decide what to do
(e.g., show an error message, use default values, etc.). The program doesn't need to crash because of it.
Built-In Exceptions in Python

Python comes with a set of built-in exceptions, which are pre-defined errors that the Python language itself can
recognize. These exceptions help the programmer handle common errors without having to create new ones.

All exceptions in Python are organized in a hierarchy of classes. The base class for all exceptions is the
BaseException class, and from this, all other exceptions are derived (i.e., "built on top of").

Here is the general structure of exception classes in Python:

1. BaseException: This is the topmost class in Python's exception hierarchy. All exceptions in Python are
subclasses of BaseException. However, you rarely deal directly with this class.
2. Exception: This is a subclass of BaseException and is the main class for most built-in exceptions. In fact, all
errors in Python are actually subclasses of this Exception class.
3. StandardError: This used to be a subclass of Exception and was the parent for most of the errors like
ValueError, IndexError, etc. However, in modern versions of Python, it’s considered somewhat
obsolete, as exceptions are now directly derived from Exception.
4. Warning: A warning is not really an error. It’s more like a caution or advisory message. A warning doesn’t
stop the program from running. It’s just a heads-up to the programmer that something may not be ideal, but
it’s not serious enough to crash the program.

Example of Built-In Exceptions

Here are some examples of built-in exceptions in Python:

 ZeroDivisionError: Raised when you try to divide a number by zero.


 IndexError: Raised when you try to access an index that doesn’t exist in a list or other sequence.
 ValueError: Raised when a function receives an argument of the wrong type or value.
 FileNotFoundError: Raised when you try to open a file that doesn’t exist.

User-Defined Exceptions
In addition to the built-in exceptions, Python also allows programmers to create their own custom exceptions. These
are called user-defined exceptions. If you have a specific error that you want to catch in your program that isn’t
already covered by Python’s built-in exceptions, you can create your own exception class.

To do this, you should inherit from the Exception class, not from BaseException. Here's an example of a user-
defined exception:

Example of User-Defined Exception


# Define a custom exception class
class InvalidAgeError(Exception):
def __init__(self, message):
self.message = message
super().__init__(self.message)

# Function that uses the custom exception


def check_age(age):
if age < 18:
raise InvalidAgeError("Age must be 18 or older")
else:
print("Age is valid.")

# Testing the custom exception


try:
check_age(16) # This will raise the custom exception
except InvalidAgeError as e:
print(f"Error: {e}")

Output:

Error: Age must be 18 or older

Explanation:

 Here, we created a custom exception class called InvalidAgeError, which inherits from the Exception
class.
 When we try to check the age and if it’s below 18, the InvalidAgeError exception is raised with a custom
message.
 The try and except block is used to catch this exception and handle it appropriately (in this case, by
printing an error message).

Why Handle Exceptions?

Handling exceptions is important because it helps you manage errors gracefully without crashing the program.
Instead of the program stopping abruptly, the programmer can define what should happen when an exception
occurs, whether it’s displaying an error message, using a default value, or even trying the operation again.

For example, if you're dealing with file operations, and the file doesn't exist, you can handle this exception by telling
the user the file isn’t found instead of letting the program crash unexpectedly.

Warnings vs Errors

 Warnings: These are not critical and do not stop the program. They’re just there to inform you that
something may not be ideal. For example, if you use a deprecated feature in a library, Python might give you
a warning, but your program will continue running.
 Errors: These are critical problems that stop the program from running correctly. You must handle these
errors to prevent your program from crashing. For example, trying to divide by zero is an error that will stop
the program if not handled.
Summary

 Exceptions are runtime errors that can be handled by the programmer to prevent the program from
crashing.
 Built-in exceptions are provided by Python and include things like ZeroDivisionError, ValueError, etc.
 Warnings are less severe and don't stop the program from running, but errors do.
 User-defined exceptions allow the programmer to create their own exceptions when the built-in ones don't
cover their needs.

Exception Handling
What is Exception Handling?

Exception handling is a way to handle errors in a program so that the program doesn't crash or stop unexpectedly
when something goes wrong. Instead, when an error (or "exception") happens, the program can continue running
smoothly by managing the error in a controlled way.

Imagine you’re writing a program that performs some calculations. Sometimes, errors like dividing by zero, accessing
a file that doesn’t exist, or entering invalid data may occur. Without exception handling, the program would crash.
But by handling exceptions, you can catch the error, show a helpful message, and continue running the program.

Why is Exception Handling Important?

 Robustness: A program becomes "robust" (strong and reliable) when it can handle errors gracefully instead
of stopping abruptly. This is crucial in real-world applications because unexpected things often happen (like a
user entering wrong data, or the internet connection being lost).
 Error Message to User: When an exception occurs, you can show the user a helpful message explaining the
error and how to avoid it in the future.
 Preventing Data Loss: When an exception occurs, the finally block can be used to clean up, close files, and
release resources properly. This ensures that important data is not corrupted or lost.

How Does Exception Handling Work?

Python allows you to handle exceptions using the following blocks:

1. try block: The code that might cause an exception goes here. It's like saying, "I'm going to try this, but if
something goes wrong, handle it."
2. except block: If an exception occurs, Python jumps to this block, and the programmer can specify what
should happen when that exception happens. This is the "handler" for the exception.
3. finally block: No matter what happens (whether there is an exception or not), the finally block will always
run. It’s mainly used for cleanup actions, like closing files or stopping processes.

Three Steps in Exception Handling

Step 1: Identify Code That Might Cause Errors

The first thing the programmer does is identify the parts of the code that might cause an error. These sections are
placed inside the try block.

try:
# Statements that might raise an exception
For example, division by zero or trying to open a non-existent file might cause errors, so these go inside the try
block.

Step 2: Handle Exceptions

If an error happens in the try block, Python jumps to the except block. Here, the programmer can handle the error
by showing a message, fixing the problem, or taking action.

except exceptionname:
# Handle the error here

The programmer can specify what type of exception they want to handle (like ZeroDivisionError for division by
zero), and they can provide a message for the user explaining what went wrong and how to fix it.

Step 3: Clean Up (finally block)

The finally block contains code that should always run, no matter what. This is useful for cleaning up resources
like files, network connections, or processes.

finally:
# Always run this code (e.g., closing files)

Even if there was no exception, the finally block will run. If an exception occurred, it will still run, ensuring that
things like file handles are properly closed.

Example of Exception Handling

Here is a Python program that handles a division by zero error:

try:
# Try to open a file and perform division
f = open("myfile", "w")
a, b = [int(x) for x in input("Enter two numbers: ").split()]
c = a / b # This might cause a ZeroDivisionError
f.write("writing %d into myfile" % c)
except ZeroDivisionError:
print('Division by zero happened') # Handle division by zero error
print('Please do not enter 0 in input')
finally:
f.close() # Always close the file
print('File closed')

Output:

1. When valid input is entered (e.g., 10 and 2):

File closed

2. When division by zero happens (e.g., 10 and 0):

Division by zero happened


Please do not enter 0 in input
File closed

In both cases, the finally block ensures that the file is closed, regardless of whether there was an error or not.

Extended Exception Handling Syntax


The full structure of exception handling in Python looks like this:

try:
# Code that might cause exceptions
except Exception1:
# Handle Exception1
except Exception2:
# Handle Exception2
else:
# Code to run if no exception occurs
finally:
# Code that runs no matter what (cleanup)

Key Points:

 Multiple except blocks: You can handle different exceptions separately with multiple except blocks. For
example, one for handling division errors and another for handling file-related errors.
 else block: If no exception is raised, the code inside the else block runs. It's not mandatory but is useful
when you want to do something only if no error occurred.
 finally block: This is always executed, whether an exception occurred or not. It's used for cleanup actions,
such as closing files or releasing resources.

Example with Multiple except Blocks


try:
a, b = [int(x) for x in input("Enter two numbers: ").split()]
c = a / b
except ZeroDivisionError:
print("Error: Division by zero.")
except ValueError:
print("Error: Invalid input, please enter integers.")
else:
print(f"Result: {c}")
finally:
print("Execution complete.")

Output Examples:

 Valid input (e.g., 10 2):

Result: 5.0
Execution complete.

 Division by zero (e.g., 10 0):

Error: Division by zero.


Execution complete.

 Invalid input (e.g., 'a b'):

Error: Invalid input, please enter integers.


Execution complete.

Summary:

1. try block: Used for code that might raise an exception.


2. except block: Catches and handles exceptions that occur in the try block.
3. finally block: Always runs, used for cleanup actions (like closing files).
4. else block: Runs if no exception occurred, but it's optional.
Types of Exceptions

📚 What is an Exception in Python?


An exception means an error that happens while the program is running.

 If an error happens, Python will stop the program unless we handle (manage) the error properly.
 To handle exceptions, we use try, except, and sometimes else, finally blocks.

🎯 Types of Exceptions
There are two types of exceptions:

1. Built-in Exceptions (already available in Python)


2. User-defined Exceptions (we can create our own)

🏗️ Built-in Exceptions
Python already has many common exceptions ready for us.
Here’s a simple table with important ones:

Exception Name When it Happens Example

Exception The base class for all exceptions General parent for all errors

ArithmeticError Error in math calculation Divide by zero

AssertionError When assert statement fails assert 2+2 == 5

AttributeError Accessing a non-existing attribute my_list.name

EOFError Input reaches End of File Reading input but no data

FloatingPointError Floating-point operation error Rare math error

GeneratorExit Generator closed by close() Generator stops

IOError Input/Output file error File not found

ImportError Import fails import nonexisting_module


Exception Name When it Happens Example

IndexError Index out of list range list[100]

KeyError Key not found in dictionary my_dict['missing_key']

KeyboardInterrupt Press Ctrl+C during execution

NameError Variable not defined print(x) when x doesn't exist

OverflowError Math result too big Very large number

RuntimeError Any unknown error during runtime

StopIteration No more items in iterator

SyntaxError Wrong Python syntax Forgetting a colon :

IndentationError Wrong indentation (spaces)

SystemExit sys.exit() is called Program stops

TypeError Wrong type of data used Add string + int

UnboundLocalError Local variable used before assignment

ValueError Correct type, wrong value int('abc')

ZeroDivisionError Dividing by zero 5/0


🔥 Now let's go through 3 Programs that show how
to handle exceptions!
🛠️ Program 8: Handle SyntaxError using eval()
 eval() function takes input like a tuple, list, or dictionary and evaluates it.
 If the user enters wrong input (like letters with numbers), a SyntaxError will happen.

✅ Program:

# Example for Syntax Error


try:
date = eval(input("Enter date: ")) # Example input: 2016, 10, 3
except SyntaxError:
print('Invalid date entered')
else:
print('You entered: ', date)

✅ Example Output:

Correct Input:

Enter date: 2016, 10, 3


You entered: (2016, 10, 3)

Wrong Input:

Enter date: 2016, 10b, 3


Invalid date entered

🎯 Simple Explanation:

 If you type correctly (numbers separated by commas), it shows the date.


 If you type wrongly (adding letters), it catches the error and shows "Invalid date entered".

🛠️ Program 9: Handle File Not Found Error (IOError)


 Using the open() function to open a file.
 If the file does not exist, IOError happens.

✅ Program:

# Example for IOError


try:
name = input('Enter filename: ') # Example: ex.py
f = open(name, 'r') # Try to open the file in read mode
except IOError:
print('File not found: ', name)
else:
n = len(f.readlines()) # Count the number of lines
print(name, 'has', n, 'lines')
f.close()

✅ Example Output:

File Exists:
Enter filename: ex.py
ex.py has 11 lines

File Does NOT Exist:

Enter filename: abcd


File not found: abcd

🎯 Simple Explanation:

 If the file exists, it tells how many lines are inside.


 If the file is missing, it says "File not found".

🛠️ Program 10: Handle Multiple Errors (TypeError,


ZeroDivisionError)
 A function avg() is created to calculate total and average from a list.
 If wrong data like string is inside the list ➔ TypeError happens.
 If the list is empty ➔ ZeroDivisionError happens.

✅ Program:

# Example for two exceptions


def avg(list):
tot = 0
for x in list:
tot += x # Adds all elements
avg = tot / len(list) # Finds average
return tot, avg

try:
t, a = avg([1, 2, 3, 4, 5, 'a']) # Try also avg([]) for empty list
print('Total= {}, Average= {}'.format(t, a))
except TypeError:
print('Type Error, please provide numbers.')
except ZeroDivisionError:
print('ZeroDivisionError, please do not give empty list.')

✅ Example Output:

Passing wrong data (string inside list):

Type Error, please provide numbers.

Passing empty list:

ZeroDivisionError, please do not give empty list.

🎯 Simple Explanation:

 If you give a letter in the list ➔ "Type Error" message.


 If you give an empty list ➔ "ZeroDivisionError" message.
🧠 Important Concepts from These Programs
Concept Meaning

try Code that can cause error goes here

except If an error occurs, this code runs

else If no error occurs, this code runs

Multiple except blocks To handle different types of errors separately

Handling Errors Prevent program from crashing and give a good message

📝 Summary
✅ Python has many built-in exceptions.
✅ We can catch and handle exceptions using try and except.
✅ We can handle different types of errors differently.
✅ Programs become stronger and more professional when we handle exceptions.

The except Block in Python


When something wrong happens inside the try block, Python immediately jumps to the except block.
The except block catches the error and handles it nicely instead of crashing the program.

🛠️ Different Ways to Write except Blocks


Python allows us to write the except block in many ways depending on how we want to catch errors.

1. 🎯 Catch a Specific Exception


We mention the specific exception name after except.

✅ Format:

except ExceptionClass:

✅ Example:
try:
a = 5 / 0
except ZeroDivisionError:
print("You cannot divide by zero!")

🔵 Meaning: If a ZeroDivisionError happens, this except block will catch it.

2. 🎯 Catch Exception with an Object (to know more details)


We can catch the error as an object. This object stores extra information about the error.

✅ Format:

except ExceptionClass as obj:

✅ Example:

try:
a = 5 / 0
except ZeroDivisionError as e:
print("Error occurred:", e)

🔵 Meaning:

 e stores the actual error message: division by zero.


 We can print or use it.

3. 🎯 Catch Multiple Exceptions Together


Sometimes, more than one type of error might happen.
Instead of writing many except blocks, we can combine them using a tuple (parentheses).

✅ Format:

except (ExceptionClass1, ExceptionClass2, ...):

✅ Example:

try:
a = int("abc") # ValueError
b = 5 / 0 # ZeroDivisionError
except (ValueError, ZeroDivisionError):
print("Either ValueError or ZeroDivisionError occurred.")

🔵 Meaning: If either error happens, the same block will handle it.

4. 🎯 Catch Any Exception (Don't Care Which One)


If you don't care which error happened and just want to handle it, you can leave out the exception name.

✅ Format:

except:

✅ Example:

try:
a = 5 / 0
except:
print("Some error occurred.")

🔵 Meaning:

 No matter what error occurs, the program will not crash.


 But you won't know exactly what error it was.

🛠️ Previous Example (Program 10 Simplified)


Earlier in Program 10, you caught two errors separately like this:

except TypeError:
print("Type Error occurred.")
except ZeroDivisionError:
print("Zero Division Error occurred.")

Now, you can combine both like this:

except (TypeError, ZeroDivisionError):


print("Either TypeError or ZeroDivisionError occurred.")

🔵 Advantage: Shorter and cleaner code!

✨ Important Point
 If you don't mention which error, it will catch any error.
 But best practice: Always mention the exception names if possible, so you know what went wrong!

📚 Now about try with finally (Program 11)

🛠️ try without except (only finally)


Normally, when we use try, we should have except to catch errors.
But — sometimes you can skip except and use finally instead.

✅ Important Rule:

 If you use try without except, you must use finally.


 finally always runs — no matter if an error happens or not.

✅ Program 11: Example

# try without except block


try:
x = int(input('Enter a number: '))
y = 1 / x
finally:
print("We are not catching the exception.")
print("The inverse is: ", y)

✅ What happens here?

1. It asks you to enter a number.


2. It tries to calculate 1 divided by that number.
3. Even if an error happens (like dividing by zero), the finally block will still run.

✅ Example 1: Normal Case

You enter: 5

 1/5 = 0.2
 Output will be:

We are not catching the exception.


The inverse is: 0.2

✅ Example 2: Error Case

You enter: 0

 1/0 ➔ ZeroDivisionError will occur!


 Python will crash after running the finally block.
 Output will be:

We are not catching the exception.

Then, Python shows an error message like:

ZeroDivisionError: division by zero


🧠 Summary of Program 11
Part Meaning

try Attempts risky code (dividing numbers)

no except No special catching of errors

finally Always runs, even if error occurs

Risk Program will crash after finally if error is not caught

🔥 Very Simple Flow of Program 11


Start Program

try:
Ask for a number
Try to divide 1 by number

finally:
Always print "We are not catching the exception."
Always try to print inverse

If error happened ➔ Crash after finally
If no error ➔ Normal end

🎯 Final Important Tips


Tip Reason

Use try + except To catch and handle errors smoothly

Use try + finally To clean up work (like closing files) even if error happens

Always mention exception name in except To know which error occurred

Only use empty except: if really needed Otherwise it's hard to debug errors

The assert Statement in Python


✅ Meaning in simple words:

 assert is like saying: "I am sure this must be true!"


 If the condition is true, the program continues normally.
 If the condition is false, Python stops and gives an error called AssertionError.
📌 Syntax
assert condition, message
Part Meaning

condition What you want to check (e.g., x > 5)

message (optional) What you want to say if the condition is wrong

🧠 How assert works


 If condition is True → No problem, move to next line.
 If condition is False → Raise AssertionError, show message if you gave one.

🛠️ Program 12 (Without Message)


Let's understand with the program you shared:

# handling AssertionError
try:
x = int(input('Enter a number between 5 and 10: '))
assert x >= 5 and x <= 10
print('The number entered: ', x)
except AssertionError:
print('The condition is not fulfilled')

✅ Step-by-step explanation:
1. It asks the user to enter a number between 5 and 10.
2. assert x >= 5 and x <= 10 checks if the number is really between 5 and 10.
3. If you entered correctly, it prints the number.
4. If you entered wrong (like 12 or 3), it throws an error and the except block catches it, printing:

The condition is not fulfilled

✅ Example Outputs:
 If you enter 7 ➔ Output:

The number entered: 7

 If you enter 12 ➔ Output:

The condition is not fulfilled


🛠️ Program 13 (With Message)
Now, same program but with a custom message when condition fails:

# handling AssertionError - v 2.0


try:
x = int(input('Enter a number between 5 and 10: '))
assert x >= 5 and x <= 10, "Your input is not correct"
print('The number entered: ', x)
except AssertionError as obj:
print(obj)

✅ Step-by-step explanation:
1. User enters a number.
2. assert checks if the number is between 5 and 10.
3. If not, it raises an error with your custom message: "Your input is not correct".
4. except block catches the error and prints the message.

✅ Example Outputs:
 If you enter 9 ➔ Output:

The number entered: 9

 If you enter 2 ➔ Output:

Your input is not correct

🎯 Simple Real-Life Example of assert


Imagine you are giving someone entry into a movie theater:

 You want to allow only 18+ people.


 If someone is younger, you want to stop them immediately.

✅ Python code:

age = int(input("Enter your age: "))


assert age >= 18, "You must be 18 or older to watch this movie."
print("Welcome to the movie!")

🧠 Important Things to Remember


Point Why Important

Use assert when you are very sure that a condition must be true. Example: age limits, input validations

assert helps in finding bugs early. It immediately stops wrong programs.

If you give a message, it becomes easier to understand what went


Easier debugging
wrong.

If assert fails and no one catches the error, the program will So you can use try-except to catch it if
crash. needed.

🌟 Summary: Super Simple Table


Concept In Easy Words

assert "This must be true!"

If True Program goes on

If False Program stops with AssertionError

With Message Shows your custom message

Without Message Shows simple AssertionError

Logging the Exceptions


🌟 What is Logging?
 When our Python program runs, it can sometimes face problems called exceptions or errors.
 Instead of just showing these errors on the screen, we can save them into a file (called a log file).
 This saving process is called "logging".

✅ Why logging is useful?

 Later, we can open the file, read all the errors, find out where things went wrong, and fix them easily.
 It helps in debugging (finding and solving mistakes in the program).

📋 Different Error Levels in Logging


In Python, errors or messages have levels based on how serious they are:
Level Value Meaning

CRITICAL 50 Very big problem (like system crash) 🚨

ERROR 40 Serious error 😨

WARNING 30 Just a warning. Be careful! ⚠️

INFO 20 Useful information ℹ️

DEBUG 10 Small details for debugging 🔍

NOTSET 0 Level is not set ❔

✅ By default, Python only shows messages from WARNING level and above (WARNING, ERROR, CRITICAL).

🛠️ How to Start Logging in Python?


First, we use the logging module. We tell it two things:

1. Filename where messages will be saved.


2. Level from where saving should start (like only errors and above).

import logging
logging.basicConfig(filename='mylog.txt', level=logging.ERROR)

 filename='mylog.txt' → The file where messages will be stored.


 level=logging.ERROR → Only errors with level ERROR or higher (ERROR, CRITICAL) will be stored.

✍️ How to Write Messages?


We can now write different types of messages:

logging.error("There is an error in the program.")


logging.critical("System crash - Immediate attention required")
logging.warning("The project is going slow.")
logging.info("You are a junior programmer.")
logging.debug("Line no. 10 contains syntax error.")

✅ Only the error and critical messages are stored because we set level to ERROR. ✅ warning, info, debug
will NOT be saved in the file.

✨ Example Program 1 (Program 15)


Code:

import logging
logging.basicConfig(filename='mylog.txt', level=logging.ERROR)

logging.error("There is an error in the program.")


logging.critical("There is a problem in the design.")
logging.warning("The project is going slow.")
logging.info("You are a junior programmer.")
logging.debug("Line no. 10 contains syntax error.")

What happens:

 File mylog.txt is created.


 It saves only:

ERROR:root:There is an error in the program.


CRITICAL:root:There is a problem in the design.

 Other messages are ignored because they are below ERROR level.

🧠 Making it Easier
Instead of writing logging.error() every time, we can import everything:

from logging import *

basicConfig(filename='mylog.txt', level=ERROR)
error("There is an error in the program.")
critical("There is a problem in the design.")

Now, no need to write logging. every time!

🧠 How to Log Exceptions (Errors That Happen During Program


Running)
When our program tries something that may cause an error, we use:

 try block → to try the code.


 except block → to catch any errors.

Inside except, we use:

logging.exception(e)

where e is the error object.

✨ Example Program 2 (Program 16)


Code:
import logging

logging.basicConfig(filename='log.txt', level=logging.ERROR)

try:
a = int(input('Enter a number: '))
b = int(input('Enter another number: '))
c = a / b
except Exception as e:
logging.exception(e)
else:
print('The result of division:', c)

🚀 How It Works
Suppose you run this program:

User Input What Happens Saved in log.txt?

10 and 20 No error, prints 0.5 No logging needed

10 and 0 ZeroDivisionError (can't divide by 0) YES

10 and ab ValueError (can't convert 'ab' to number) YES

Contents of log.txt after errors:

ERROR:root:division by zero
Traceback (most recent call last):
File "ex.py", line 9, in <module>
c = a/b
ZeroDivisionError: division by zero

ERROR:root:invalid literal for int() with base 10: 'ab'


Traceback (most recent call last):
File "ex.py", line 8, in <module>
b = int(input('Enter another number: '))
ValueError: invalid literal for int() with base 10: 'ab'

✅ Now the programmer can open log.txt and easily see what errors happened.

🧠 Final Simple Summary


 Logging = Saving error messages into a file.
 Use logging module to do it.
 Different levels of errors (critical, error, warning, info, debug).
 Store errors into a file using basicConfig().
 Handle exceptions with try-except, and save errors with logging.exception(e).

Points to Remember
✅ What is an Exception?

 An exception is a problem that happens while the program is running, but we can handle it using
Python code.
 If we cannot handle the problem, it becomes a normal error that can crash the program.

✅ When Do Exceptions Happen?

 They happen only at runtime (while the program is running).


 If there is a mistake during compiling the program (like syntax mistakes), it’s called a compilation
error, not an exception.
 If you make a mistake in logic (like writing wrong formulas), that's a logical error, also not an
exception.

✅ What Happens When an Exception Occurs?

 The program stops immediately (abruptly).


 This can cause loss of important data in files, databases, etc.

✅ How are Exceptions Represented?

 In Python, exceptions are classes.


 Every exception class comes from the BaseException class.
 All normal error exceptions are sub-classes of StandardError class.
 All warning messages (small alerts) come from the Warning class.

✅ What About User-Defined Exceptions?

 If a programmer wants to create their own special exception, they should create it by inheriting
(extending) the Exception class.

✅ Why Should We Handle Exceptions?

 If we handle exceptions properly, the program becomes robust (strong and safe).
 It will not crash, and it will work smoothly even if something unexpected happens.

✅ How Do We Handle Exceptions?

 Use these four blocks:


o try block → Code that might cause an exception.
o except block → Code that handles the exception.
o finally block → Code that always runs (even if there was an exception or not).
o else block → Code that runs only if there was no exception.

✅ Special Behaviors:

 If an exception occurs, Python immediately jumps from the try block into the except block.
 If there is no exception, Python skips except and goes to the else block.

✅ Handling Multiple Exceptions:

 You can use many except blocks for different types of exceptions.
 Or you can use one except block with multiple exception types inside a tuple (round brackets).
✅ What is Assert?

 assert is a tool to check if something is True.


 If not, Python will raise AssertionError.

✅ Built-in vs User-Defined Exceptions:

 Built-in exceptions are already available in Python (like ZeroDivisionError, ValueError, etc.).
 User-defined exceptions are created by the programmer for special cases.

✅ How to Raise an Exception Manually?

 You can use the raise statement to manually trigger an exception.

✅ What is Logging?

 Instead of just showing errors on screen, we can save them in a file called a log file.
 Logging helps to keep a record of errors, making it easy to debug and fix the problems later.

FILES IN PYTHON
📂 Files
📂 What is a File?

 A file is like a place where we store information permanently on a computer.


 Just like we store papers inside a folder or cabinet in real life, computers store data inside files.
 Example:
o In real life, a folder can have student records like name, roll number, and marks.
o On a computer, a file can have the same kind of information stored digitally.

📜 Why Are Files Important?

 Data is everything for companies and organizations.


 If a company loses its data, it might have to shut down because it can't work without important information
like customer details, employee records, bank transactions, etc.
 That’s why computers were mainly created: to store, protect, and work with data.

📂 Files in Python (or Any Computer)

 From a computer’s point of view, a file is just a collection of data.


 A program can create a file, store data into it, read from it, change it, or delete parts of it.
 We open files when we want to use the information and close them when we are done.

🎯 Advantages of Storing Data in Files

1. Permanent Storage
o When you store something in a file, it stays there even if you turn off the computer.
o Files are stored on hard disks, pen drives, CDs, or clouds.
o Example: You write a story in a text file today, you can still read it after 5 years!
2. Can Update Data
o Files are flexible.
o You can add new data, remove unwanted data, or change old data.
o Example: Add a new student to the student list, or update a salary when someone gets a promotion.
3. Data Sharing Across Programs
o The same file can be used by many programs.
o Example:
 One program uses employee data to calculate salaries.
 Another program uses the same file to calculate income tax for employees.
o No need to type data again and again — it saves time and effort.
4. Handling Big Data
o Files can store a huge amount of data easily.
o Example:
 Voter lists of an entire country.
 Census data (how many people live in a city).
o Even millions of records can be kept safely in files.

🧠 Easy way to remember:

In Real Life In Computer

A folder with papers A file with digital data

📂 Types of Files in Python


In Python (and in general), there are two types of files you can work with:

1. Text Files

 Text files store normal letters, numbers, and symbols as characters.


 Example of a text file: a file that has:

Name: Ganesh
Salary: 8900.75

 In a text file:
o "Ganesh" will be stored as 6 characters: G, a, n, e, s, h.
o "8900.75" (salary) will be stored as 7 characters: 8, 9, 0, 0, ., 7, 5.
🔵 Key point:
Text files are good when you want to store readable information like names, addresses, or notes.

2. Binary Files

 Binary files store data as bytes (small units made up of 8 bits: 1s and 0s).
 Computers love working with bytes because that's their language (1s and 0s)!
 In a binary file:
o A character or a number is stored as binary numbers (not readable by humans).
o Example: On a 64-bit computer, one number (like 100) may take up 8 bytes.
 Binary files are used to store:
o Images (like .jpg, .png files)
o Audio files (like .mp3)
o Video files (like .mp4)
o Big databases

🔵 Key point:
Binary files are good when you want to store things that computers use directly, like pictures, songs, videos —
because they are made up of lots of tiny dots (pixels) or sounds — not letters!

📷 Why can't images be stored in text files?


 A text file can only store letters, numbers, and symbols.
 An image is not made of letters, it is made of tiny dots called pixels.
 Each pixel is either on (1) or off (0).
 So, images need binary files to store this pixel information properly.

📖 How to Work with Files in Python?


Whenever you want to do anything with a file (like create it, write in it, or read from it), you must follow three
steps:

1. Open the file (using Python's open() function).


2. Perform your operation (read, write, update, etc.).
3. Close the file (so that memory is saved and data is safe).

➡️ Without opening a file first, you cannot do anything with it!

🧠 Super simple memory tip:


Text Files (📝) Binary Files (💾)

Human-readable (names, notes) Computer-readable (images, videos)


Text Files (📝) Binary Files (💾)

Stores letters and numbers Stores 0s and 1s (bits and bytes)

.txt files, .csv files .jpg, .png, .mp3, .mp4 files

📂 Opening a File in Python


When we want to work with a file (like read from it or write into it) in Python, we first need to open the file.

👉 To open a file, we use a special function called:

open()

🧠 How the open() function looks:


file_handler = open("file name", "open mode", "buffering")

What these things mean:

 "file name" → The name of the file you want to open. Example: "data.txt".
 "open mode" → What you want to do with the file:
o Read it?
o Write in it?
o Add new content at the end?
 "buffering" → (Optional) How much memory you want to use temporarily when working with the file. (You
usually don’t need to worry about it.)

🎯 Example:
f = open("myfile.txt", "w")

 f → This is called the file handler or file object.


 "myfile.txt" → This is the file we are opening (or creating if it doesn't exist).
 "w" → This is the mode, which here means "open the file for writing".

📖 File Opening Modes (Very Simple Table)


Mode What it does Example

w Write in the file. If the file already has some data, it will be deleted first. Save new employee data

r Read from the file. You cannot write. The file must already exist. Read a list of students
Mode What it does Example

a Add (append) new data at the end of the file without deleting existing data. Add new product to a list

Save a new report and check


w+ Write and then read the file. Old data will be deleted.
it

r+ Read and write. Old data is kept, and you can change it. Update a marksheet

Add (append) and read the file. If the file doesn’t exist, it will create a new
a+ Add new voters
one.

x Create a new file. If the file already exists, it will give an error. Create a new log file

🧠 Small Notes:
 Text Files → Normal files like .txt.
 Binary Files → Special files like images (.jpg, .png), music (.mp3).

If you are working with binary files, just add 'b' after the mode:

Text mode Binary mode

w → Write wb → Write binary

r → Read rb → Read binary

a → Append ab → Append binary

and so on... and so on...

🗂️ What is Buffering?
 Buffer means a small temporary memory that helps when you work with files.
 It helps read/write faster by taking data in chunks instead of little bits.
 You can tell Python how much buffer memory you want (optional):
o 0 → No buffering (direct read/write)
o 1 → Line buffering (one line at a time for text files)
o Positive number like 500 → 500 bytes buffering

If you don't mention anything, Python will automatically use 4096 or 8192 bytes buffering.

Tip: As a beginner, you can just skip setting buffering — Python will handle it well.

📋 Quick Example Summary:


# Open a file to write
f = open("data.txt", "w")

# Now 'f' is a file object pointing to 'data.txt'


# You can now write to this file using f.write()

# Always remember to close the file after work


f.close()

🧠 Simple Way to Remember:


Step What to do

1. Open the file with open()

2. Do your work (read/write)

3. Close the file with close()

📂 Closing a File in Python


When we open a file in Python (using open()), it's very important to close it after we finish working with it.
We use the close() method to do that.

✏ Why do we need to close a file?

 ✅ To save the data properly into the file.


 ✅ To free up memory. (If files stay open, they keep using the computer’s memory.)
 ✅ To avoid problems like file corruption or data loss, especially if you open many files at the same time.

👉 If you don't close a file, your program might behave badly or crash later.

✏ How to close a file?


When you're done writing or reading, just use:

f.close()

Here, f is the file object — the name you used when you opened the file.

Example:

f = open('myfile.txt', 'w') # open the file


# work with the file
f.close() # close the file
After f.close(), the file is properly saved and closed.
If you want to work on it again, you must open it again.

🖊 Writing into a File


Now, let's see a small program to create a file and store characters into it.

Program 1: Create a file and write into it


# Step 1: Open a file in write mode
f = open('myfile.txt', 'w')

# Step 2: Take input from the user


str = input('Enter text: ')

# Step 3: Write the input text into the file


f.write(str)

# Step 4: Close the file


f.close()

What happens here?

 It opens a file called myfile.txt for writing ('w' mode).


 It asks you to type something using input().
 It writes whatever you typed into the file.
 Then it closes the file properly.

✅ Important point:
If the file myfile.txt already had some old text, it will be erased. Only your new text will be saved.

➕ If you want to add new text (without deleting


old text)
Use append mode 'a' instead of 'w'.

Example:

f = open('myfile.txt', 'a')

This way, the new text will be added at the end of the old text.

📖 Reading from a File


Now, after we write into a file, how to read the data back?
We use the read() method.

Program 2: Read the file and display content


# Step 1: Open the file in read mode
f = open('myfile.txt', 'r')

# Step 2: Read all the content


str = f.read()

# Step 3: Print the content


print(str)

# Step 4: Close the file


f.close()

What happens here?

 It opens myfile.txt for reading ('r' mode).


 It reads all the text inside the file and saves it into a variable str.
 Then it prints that text on the screen.
 Finally, it closes the file.

✂️ If you want to read only some characters


You can tell Python how many characters you want to read!

Example:

str = f.read(4)

This will read only the first 4 characters from the file.

Example output:

This

If your file had "This is my first line.", only "This" will be read.

🧠 Summary in one line:


Operation Mode Meaning

Writing (overwriting) 'w' Deletes old content, writes new

Appending (adding) 'a' Adds new content after old

Reading 'r' Reads content from file

Always remember to use f.close() after your work is done! 🧡


📂 Working with Text Files Containing Strings

📝 Writing Multiple Strings into a Text File

 Suppose you want to save multiple lines (like sentences or strings) into a file.
 You can't just use write() one time.
 Instead, you need to take input again and again inside a loop.

Example Idea:

 Keep taking input from the user.


 Stop only when the user types @.
 Otherwise, save each line into the file.

Important:

 We add "\n" (new line) after every string so that each sentence comes on a new line in the file.

💻 Program 3: Saving Multiple Strings into a File


# open file to write ('w' mode)
f = open('myfile.txt', 'w')

print('Enter text (@ at end):')

while str != '@': # repeat until user types '@'


str = input() # get input from user
if str != '@': # if input is not '@'
f.write(str + "\n") # write to file with a new line

f.close() # close file

📂 What happens in this Program?

 First, we open the file myfile.txt in write mode.


 Then, ask user to type lines.
 Each time, we save what user typed into the file.
 If the user types @, we stop taking input.
 Finally, we close the file.

Example Run:

Enter text (@ at end):


This is my first line.
This is my second line.
@

In myfile.txt, it will be saved like:


This is my first line.
This is my second line.

📖 Reading Strings from the File

To read what we wrote into the file:

# open file to read ('r' mode)


f = open('myfile.txt', 'r')

print('The file contents are:')

str = f.read() # read everything


print(str)

f.close()

 f.read() reads everything and prints as it is (including new lines).

📋 Other Ways to Read the File

1. Using f.readlines()

 It reads the whole file line-by-line.


 But gives list of strings with \n at the end of each line.

Example:

['This is my first line.\n', 'This is my second line.\n']

2. Using f.read().splitlines()

 This reads lines and removes \n.


 Gives a clean list.

Example:

['This is my first line.', 'This is my second line.']

✏ Appending (Adding) New Strings to Existing File

What if you don't want to overwrite the file?


What if you want to add new lines without deleting old lines?

✅ Use a+ mode (append and read).

f = open('myfile.txt', 'a+')

💻 Program 5: Appending Strings and Displaying Entire File


# open file to append and read ('a+' mode)
f = open('myfile.txt', 'a+')

print('Enter text to append(@ at end):')

while str != '@':


str = input()
if str != '@':
f.write(str + "\n")

# move pointer to start of file


f.seek(0,0)

# read everything
print('The file contents are:')
str = f.read()
print(str)

f.close()

🔥 What's New Here?

 f.seek(0, 0) moves the cursor back to the start of file after writing.
 Then we read from the beginning and display the updated file.

✨ Example:

Suppose myfile.txt already has:

This is my first line.


This is my second line.

Now you run the program and type:

Enter text to append(@ at end):


This is my third line.
@

📃 Final content of myfile.txt:

This is my first line.


This is my second line.
This is my third line.

🎯 In Very Short:
Operation Meaning

open('filename', 'w') Write new file (overwrites old data)

open('filename', 'r') Read file

open('filename', 'a') Append new data to existing file


Operation Meaning

open('filename', 'a+') Append and Read file

Knowing Whether a File Exists or Not…


(Checking if a File Exists, and Counting Lines,
Words, and Characters)

1. Checking if a File Exists


In Python, before we try to open a file, we must check whether that file really exists or not.
Otherwise, Python will give an error if the file is missing.

To check if a file exists, we use:

import os

os.path.isfile(filename)

 It returns True if the file is there.


 It returns False if the file is missing.

Example Program:
import os, sys

fname = input('Enter filename: ')

if os.path.isfile(fname):
f = open(fname, 'r')
else:
print(fname + ' does not exist')
sys.exit() # stops the program

print('The file contents are: ')


str = f.read()
print(str)

f.close()

How this Program works step-by-step:

1. It asks the user to enter the file name.


2. It checks whether that file exists using os.path.isfile().
3. If the file exists, it opens the file and reads the data.
4. If the file does NOT exist, it shows a message and stops the program.

Example Output:

 If file is present:

Enter filename: myfile.txt


The file contents are:
This is my file line one.
This is line two.
This line is added.

 If file is missing:

Enter filename: yourfile.txt


yourfile.txt does not exist

2. Counting Lines, Words, and Characters in a File


Once the file is opened, you can count:

 How many lines are there.


 How many words are there.
 How many characters are there.

Easy logic:

 Read file line by line using a for loop.


 For each line:
o Increase the line count by 1.
o Split the line into words and count them.
o Count all the characters in the line.

Example Program:
import os, sys

fname = input('Enter filename: ')

if os.path.isfile(fname):
f = open(fname, 'r')
else:
print(fname + ' does not exist')
sys.exit()

# counters
cl = cw = cc = 0

# read line by line


for line in f:
words = line.split() # splitting line into words
cl += 1 # count lines
cw += len(words) # count words
cc += len(line) # count characters

print('No. of lines: ', cl)


print('No. of words: ', cw)
print('No. of characters: ', cc)

f.close()

How this Program works step-by-step:

1. Ask user to enter the filename.


2. Check if the file exists.
3. If it exists, open the file.
4. Initialize counters:
o cl = line count
o cw = word count
o cc = character count
5. Read the file line by line:
o line.split() splits the line into words.
o len(words) gives number of words.
o len(line) gives number of characters.
6. Print the final total counts.
7. Close the file.

Example Output:

Suppose the file myfile.txt contains:

This is my file line one.


This is line two.
This line is added.

The output will be:

No. of lines: 3
No. of words: 13
No. of characters: 61

Summary (Simple Points):


Topic Meaning

os.path.isfile(filename) Check if a file exists

sys.exit() Stop the program

f.read() Read all content


Topic Meaning

f.readline() / f.readlines() Read line-by-line

split() Break a line into words

len() Count items (words or characters)

Working with Binary Files in Python

1. What is a Binary File?


 A binary file stores data as bytes — small pieces of raw information.
 Example:
o Images (like .jpg, .png, .gif)
o Audio files (like .mp3)
o Video files (like .mp4)
o Documents (like .pdf)

Binary files are different from text files (which store normal letters, words, and sentences).

2. How to Open a Binary File?


When you want to open a file in binary mode, you add b along with r or w:

 rb = read binary file


 wb = write binary file

👉 Example:

open('filename.jpg', 'rb') # for reading


open('newfile.jpg', 'wb') # for writing

3. How to Read and Write Binary Files?


You use:

 read() → to read bytes from the file.


 write() → to write bytes into another file.
4. Program Example: Copy an Image File
Goal: Copy cat.jpg image into a new file new.jpg.

Here’s the simple code:


# open the original image for reading (binary mode)
f1 = open('cat.jpg', 'rb')

# open a new file for writing (binary mode)


f2 = open('new.jpg', 'wb')

# read all bytes from the original image


bytes = f1.read()

# write those bytes into the new image


f2.write(bytes)

# close both files


f1.close()
f2.close()

5. Step-by-Step Working of the Program


Step What happens

1 f1 opens the original file cat.jpg in read binary (rb) mode.

2 f2 opens a new file new.jpg in write binary (wb) mode.

3 f1.read() reads all the bytes from the original image.

4 f2.write(bytes) writes those same bytes into the new file.

5 f1.close() and f2.close() close the files properly.

Result:

 Now you will have a copy of cat.jpg saved as new.jpg in the same folder!

6. Important Points to Remember:


✅ When dealing with images, videos, music, always use binary mode (rb, wb).
✅ Reading and writing bytes is very fast and simple.
✅ If the original file is not in the same folder (current directory), you must give the full path.

Example:
f1 = open('C:\\rnr\\cat.jpg', 'rb')

 Double backslashes \\ are used because in Python, a single backslash \ has a special meaning.
 So, always use double \ in file paths.

7. What is "Current Directory"?


 It is the folder where your Python program is running.
 If cat.jpg is inside the same folder where copy.py is running, then you don't need to give any path.
 If the file is somewhere else, you must provide the full path.

8. Quick Diagram:
Original file: cat.jpg
|
| read() bytes
v
New file created: new.jpg (copy)

Simple Summary
Concept Meaning

Binary File File that stores data as bytes

rb mode Read a file in binary mode

wb mode Write into a file in binary mode

read() Read all bytes

write() Write bytes

Current Directory Where your Python program is located

The with Statement in File Handling

1. Problem without with


Normally, when you open a file in Python, you must remember to close it manually:

f = open('sample.txt', 'w')
f.write('some text')
f.close()

 If you forget to close it, it might cause errors or waste memory.


 If an error (exception) happens before close(), the file will stay open, which is bad.

2. Solution: Using with


Python gives a better way:
✅ with statement automatically opens the file
✅ and automatically closes it when you are done.

You don’t need to write close() manually anymore! 🎯

👉 Format:

with open('filename', 'mode') as file_object:


# do something with the file

As soon as the code inside with block finishes, Python will close the file automatically, even if there is an error!

3. Program Example 1: Writing with with


✅ Goal: Write two lines into a file sample.txt.

Code:

# Using with to open and write into a file


with open('sample.txt', 'w') as f:
f.write('I am a learner\n')
f.write('Python is attractive\n')

What happens:

 sample.txt is opened for writing (w mode).


 Two lines are written into the file.
 After the writing is done, Python automatically closes the file.
 You don't have to write f.close()!

4. Program Example 2: Reading with with


✅ Goal: Read the lines we wrote into sample.txt and print them.
Code:

# Using with to open and read from a file


with open('sample.txt', 'r') as f:
for line in f:
print(line)

What happens:

 sample.txt is opened for reading (r mode).


 Python reads each line one by one.
 It prints each line.
 After reading, Python automatically closes the file!

Output:

I am a learner
Python is attractive

5. Step-by-Step Working of with


Step What happens

1 open() function is called inside with to open the file.

2 You write or read data inside the with block.

3 As soon as the block ends, Python automatically closes the file.

✅ No chance of forgetting to close!


✅ No problem even if an error happens inside!

6. Why is with Better?


Without with With with

You must call close() manually. Python closes the file automatically.

Easy to forget to close. No worry about closing.

Error in program → file may stay open. Error happens → file still closed properly.

Longer code. Shorter and cleaner code.

7. Simple Diagram:
Without 'with':
open file -> write/read -> close file manually

With 'with':
open file -> write/read -> Python closes file automatically!

Summary Table
Concept Meaning

with open('filename', 'mode') as f: Opens file and closes it automatically

w mode Write into file (overwrites if file exists)

r mode Read from file

\n Adds a new line when writing

Final Line
Using with while working with files is the best practice in Python!
It makes your code safe, short, and clean. ✅✨

Pickle in Python
What is Pickling in Python?

Pickling is the process of converting a Python object into a byte stream (a series of bytes). This allows you to store
the object in a file or send it over a network. Later, you can read this byte stream and reconstruct the original Python
object. This is useful because it lets you save Python objects (like lists, dictionaries, instances of a class, etc.) to a file
and retrieve them later, preserving their structure and data.

What is Unpickling?

Unpickling is the reverse of pickling. It is the process of converting a byte stream (stored in a file or received from
another source) back into the original Python object. This process allows you to reconstruct Python objects from the
data stored in files.

In simple terms:

 Pickling = Converting a Python object into a stream of bytes (to store or transfer).
 Unpickling = Converting the byte stream back into the original Python object (to use or display).

When to Use Pickling and Unpickling?

You use pickling when:


 You want to save complex data (like class objects) into a file.
 You need to save an object to be used later (e.g., saving the state of a game, user data, etc.).
 You want to transfer complex data between different Python programs or over the network.

You use unpickling when:

 You want to load the saved object (that was pickled) back into memory so you can use it again.

Pickling Example: Storing Employee Data

Let’s take an example where we want to store employee data. We’ll create an Emp class with employee details like
id, name, and salary, and then pickle (serialize) this object into a binary file.

1. Create the Emp Class

The Emp class represents the employee's details. It contains:

 id: The employee’s identification number.


 name: The employee’s name.
 sal: The employee’s salary. It also has a display method to print the employee's details in a formatted
manner.

class Emp:
def __init__(self, id, name, sal):
self.id = id
self.name = name
self.sal = sal

def display(self):
# Print employee details in a formatted way
print("{:5d} {:20s} {:10.2f}".format(self.id, self.name, self.sal))

 __init__(self, id, name, sal) is the constructor method, which initializes the employee’s ID, name,
and salary when an Emp object is created.
 display(self) is a method to display the details of an employee in a structured format (ID, name, salary).

2. Pickling (Serializing) the Object

Now, we will create a program that will:

 Take input for multiple employees (id, name, and salary).


 Create an Emp object for each employee.
 Pickle each Emp object and save it into a file named emp.dat.

import Emp, pickle

# Open the file to store employee objects in binary format


with open('emp.dat', 'wb') as f:
n = int(input('How many employees? ')) # Get the number of employees
for i in range(n):
id = int(input('Enter id: '))
name = input('Enter name: ')
sal = float(input('Enter salary: '))

# Create an Emp object with the provided data


e = Emp.Emp(id, name, sal)

# Use pickle to store the Emp object into the file


pickle.dump(e, f)
Here’s what each part does:

1. with open('emp.dat', 'wb') as f:: This line opens the emp.dat file in write-binary mode ('wb').
This is important because pickling works with binary files.
2. n = int(input('How many employees? ')): This asks the user how many employees they want to
enter.
3. for i in range(n):: This loop runs for each employee. It collects the employee’s id, name, and
salary.
4. e = Emp.Emp(id, name, sal): This creates an Emp object e with the data the user provided.
5. pickle.dump(e, f): This serializes the Emp object e and writes it into the file emp.dat.

After running this program, the employee details will be stored in the binary file emp.dat in the form of pickled
objects.

3. Unpickling (Deserializing) the Object

Once the employee objects have been pickled into the file, we can read (unpickle) the objects from the file at any
time.

Here’s the program that unpickles the data from the file and displays it:

import Emp, pickle

# Open the file to read the pickled employee objects


with open('emp.dat', 'rb') as f:
print('Employee details: ')
while True:
try:
# Load (unpickle) the next Emp object from the file
obj = pickle.load(f)

# Call the display method to show the employee details


obj.display()
except EOFError:
# When we reach the end of the file, stop the loop
print('End of file reached...')
break

Here’s what happens in this program:

1. with open('emp.dat', 'rb') as f:: This opens the emp.dat file in read-binary mode ('rb'), so we
can read the pickled objects.
2. while True:: This loop continuously reads objects from the file.
3. obj = pickle.load(f): This reads (unpickles) the next object from the file and stores it in obj. If there
are no more objects left to read, it throws an EOFError (End of File Error).
4. obj.display(): This calls the display() method on the unpickled object to print the employee’s details.
5. except EOFError:: If we reach the end of the file (no more objects), this catches the error and breaks the
loop.

The output will show each employee’s details that were saved earlier:

Employee details:
100 Sai Kumar 9000.00
101 Ravi Teja 8900.50
102 Harsh Deep 12000.75
End of file reached...

Key Points to Remember:


1. Pickle module: The pickle module in Python is used for serializing and deserializing Python objects. It
allows you to store objects in files in a format that can be read back later.
2. Pickling:
o Use pickle.dump(object, file) to store an object in a binary file.
o This converts the object into a byte stream and writes it to the file.
3. Unpickling:
o Use pickle.load(file) to read an object from a file and reconstruct it in its original form.
o This converts the byte stream back into the Python object.
4. Binary files:
o Pickle works with binary files, so always use 'wb' for writing and 'rb' for reading files.
5. End of File:
o When unpickling objects, use a try-except block to handle the EOFError, which indicates that
you've reached the end of the file and there are no more objects to read.

Why Use Pickle?

 Convenience: Pickling makes it easy to save complex objects like class instances (with various data types)
and later retrieve them.
 Data Persistence: Pickling allows you to store the state of an application or data across different program
runs.
 Network or File Transfer: It’s useful for sending Python objects over a network or saving them to a file for
future use.

Security Warning:

 Pickle is not secure: You should never unpickle data from untrusted sources, as it can execute arbitrary code
during unpickling. Always ensure the data is from a trusted source before unpickling it.

…Half…

You might also like