Python is one of the most popular programming languages in the world. It is known for its simplicity, readability, and versatility, making it a great choice for beginners as well as professionals.

  • Easy to Learn: Python uses a clean and simple syntax that looks very close to English.
  • General Purpose: It can be used for web development, data analysis, artificial intelligence, machine learning, automation, game development, and more.
  • Cross-Platform: Python works on Windows, macOS, Linux, and even mobile devices.
  • Large Community: Millions of developers contribute to Python, creating thousands of libraries and frameworks.
1. Introduction
2. Installation
3. Basic Syntax
4. Variables
5. Data Types
6. Operators
7. Input/Output
8. Conditional
9. Loops
10. Functions
11. Lists
12. Tuples
13. Sets
14. Dictionaries
15. Strings
16. File Handling
17. Exception Handling
18. Modules
19. Classes & OOP
20. Advanced Topics

1. Introduction to Python

Python is one of the most popular programming languages in the world. It is easy to learn, simple to use, and extremely powerful. Python is used in web development, artificial intelligence, data science, machine learning, automation, and more.

1.1 Why Python?

  • Beginner-friendly syntax (almost like English).
  • Supports multiple programming paradigms (object-oriented, procedural, functional).
  • Has a huge standard library and third-party modules.
  • Cross-platform: works on Windows, Linux, macOS.

1.2 Example: First Python Command

You can try your first Python program using the print() function:

print("Hello, World!")

When you run this, Python will display:

Hello, World!

1.3 Python as a Calculator

>>> 10 + 5
15
>>> 8 * 3
24
>>> 20 / 4
5.0

2. Installation & Setup

Before using Python commands, you need to install Python on your system.

2.1 Downloading Python

Go to the official website: python.org and download the latest version suitable for your operating system.

2.2 Installation on Windows

  1. Run the downloaded installer.
  2. Check the box "Add Python to PATH".
  3. Click Install Now.

2.3 Installation on macOS

Mac usually comes with Python pre-installed. You can check the version by running:

python3 --version

2.4 Installation on Linux

sudo apt update
sudo apt install python3

2.5 Verifying Installation

Once installed, check if Python is working:

python --version
# or
python3 --version

2.6 Running Python

You can run Python in two main ways:

  • Interactive mode: Type python or python3 in terminal/command prompt.
  • Script mode: Write your code in a file (e.g., hello.py) and run it with:
python hello.py

3. Basic Syntax

Before writing complex programs, you should understand the basic syntax rules of Python. These rules define how Python code must be written and interpreted.

3.1 Case Sensitivity

Python is case-sensitive. That means Print and print are considered different.

print("hello")   # works
Print("hello")   # error

3.2 Indentation

Unlike other programming languages that use braces { }, Python uses indentation (spaces or tabs) to define code blocks.

if 5 > 2:
    print("Five is greater than two")

3.3 Comments

Comments are ignored by Python but help humans understand the code.

  • Single-line comment: Starts with #
  • Multi-line comment: Written using triple quotes ''' ''' or """ """
# This is a single-line comment

"""
This is a
multi-line comment
in Python
"""

3.4 Print Statements

The print() function is used to display output.

print("Hello, Python!")
print(10 + 20)

3.5 Python Identifiers

Identifiers are names given to variables, functions, classes, etc. Rules:

  • Must start with a letter or underscore.
  • Cannot start with a digit.
  • Case-sensitive (age and Age are different).
  • Cannot use reserved keywords (like if, while, etc.).

3.6 Example Program

Here’s a simple Python program that uses comments, variables, and print statements:

# Example program
name = "Arjun"
age = 25

print("My name is", name)
print("I am", age, "years old")

4. Variables in Python

Variables are containers that store data values. In Python, you don’t need to declare the type of variable explicitly — Python automatically assigns the type based on the value.

4.1 Creating Variables

Assign a value using the = operator.

x = 10
y = "Hello"
z = 3.14

print(x)
print(y)
print(z)

4.2 Variable Naming Rules

  • Names must begin with a letter (a–z, A–Z) or an underscore (_).
  • Names cannot start with a digit.
  • Names are case-sensitive (name, Name, and NAME are different).
  • Cannot use Python keywords (e.g., if, for, class).
myvar = "John"
my_var = "Doe"
_myVar = "Python"
MyVar = 25

4.3 Multiple Assignments

You can assign values to multiple variables in one line.

a, b, c = 5, 10, 15
print(a, b, c)

You can also assign the same value to multiple variables:

x = y = z = "Python"
print(x, y, z)

4.4 Variable Types

Python supports different types of variables:

  • Numbers: integers, floats, complex
  • Strings: text inside quotes
  • Boolean: True or False
  • Collections: list, tuple, dictionary, set
x = 100          # integer
y = 25.75        # float
z = 2 + 3j       # complex number
s = "Python"     # string
flag = True      # boolean

4.5 Dynamic Typing

Python allows you to change the type of a variable by assigning a new value.

x = 10        # integer
print(type(x))

x = "Hello"   # string
print(type(x))

4.6 Global and Local Variables

A variable declared inside a function is local, and outside is global.

x = "global"

def my_func():
    x = "local"
    print("Inside:", x)

my_func()
print("Outside:", x)

4.7 The global Keyword

Use global keyword to modify a global variable inside a function.

x = "awesome"

def change_global():
    global x
    x = "fantastic"

change_global()
print("Python is", x)

4.8 Deleting Variables

Use del to delete a variable.

x = 50
print(x)
del x
# print(x)  # This will cause an error because x is deleted

4.9 Constants in Python

Python doesn’t have real constants, but by convention, variables written in uppercase are treated as constants.

PI = 3.14159
GRAVITY = 9.8

print(PI)
print(GRAVITY)

4.10 Best Practices for Variables

  • Use meaningful names (age is better than a).
  • Follow snake_case convention (e.g., student_name).
  • Use uppercase for constants.
  • Avoid single-letter names except for counters (i, j, k).

5. Data Types in Python

Data types tell Python what kind of value a variable holds. Python is dynamically typed, meaning you don’t need to declare the data type explicitly — Python assigns it based on the value.

5.1 Basic Data Types

  • int: Integer numbers
  • float: Decimal numbers
  • complex: Numbers with real and imaginary parts
  • str: Text (string)
  • bool: Boolean values (True/False)
  • NoneType: Represents absence of value
x = 10          # int
y = 3.14        # float
z = 2 + 5j      # complex
s = "Python"    # string
b = True        # boolean
n = None        # NoneType

print(type(x))
print(type(y))
print(type(z))
print(type(s))
print(type(b))
print(type(n))

5.2 Numeric Types

Python supports three numeric types: int, float, and complex.

a = 5       # int
b = 2.7     # float
c = 3 + 4j  # complex

print(a + b)
print(c.real)   # real part
print(c.imag)   # imaginary part

5.3 Strings

Strings are sequences of characters enclosed in quotes.

text1 = 'Hello'
text2 = "World"
text3 = """This is
a multi-line string"""

print(text1, text2)
print(text3)

String Operations

name = "Python"
print(len(name))          # length
print(name.upper())       # uppercase
print(name.lower())       # lowercase
print(name[0])            # indexing
print(name[0:3])          # slicing
print("Py" in name)       # membership

5.4 Boolean Type

Boolean values represent truth (True or False).

x = 5
y = 10

print(x > y)   # False
print(x < y)   # True
print(bool("Hello"))   # True (non-empty string)
print(bool(""))        # False (empty string)

5.5 None Type

None represents the absence of a value.

value = None
print(value)
print(type(value))

5.6 Collection Data Types

Python provides powerful built-in collections to store multiple items.

  • list: Ordered, mutable collection
  • tuple: Ordered, immutable collection
  • set: Unordered, unique elements
  • dict: Key-value pairs

Lists

fruits = ["apple", "banana", "cherry"]
print(fruits)
fruits.append("mango")
print(fruits[1])

Tuples

colors = ("red", "green", "blue")
print(colors)
print(colors[0])

Sets

numbers = {1, 2, 3, 3, 4}
print(numbers)   # duplicates are removed
numbers.add(5)
print(numbers)

Dictionaries

student = {"name": "Amit", "age": 20, "grade": "A"}
print(student["name"])
student["age"] = 21
print(student)

5.7 Type Conversion

You can convert between data types using built-in functions.

x = 10
y = float(x)   # int → float
z = str(x)     # int → string
a = int("25")  # string → int

print(y, type(y))
print(z, type(z))
print(a, type(a))

5.8 Checking Data Type

Use type() or isinstance() to check a variable’s type.

x = [1, 2, 3]
print(type(x))
print(isinstance(x, list))

5.9 Summary Table

Type Description Example
int Integer numbers 10, -5
float Decimal numbers 3.14, -0.5
complex Numbers with real and imaginary part 2+3j
str String (text) "Hello"
bool Boolean (True/False) True
list Ordered, mutable collection [1, 2, 3]
tuple Ordered, immutable collection (1, 2, 3)
set Unordered, unique collection {1, 2, 3}
dict Key-value pairs {"a":1, "b":2}
NoneType Represents no value None

6. Operators in Python

Operators are special symbols that perform computations or operations on values and variables. Python provides a rich set of operators grouped by purpose — arithmetic, assignment, comparison, logical, bitwise, membership, identity and more. This section explains each category, shows examples, and highlights common pitfalls and best practices.

6.1 Operator categories (quick overview)

  • Arithmetic: +, -, *, /, //, %, **
  • Assignment: =, augmented: +=, -=, *=, etc.
  • Comparison: ==, !=, <, >, <=, >=
  • Logical: and, or, not
  • Bitwise: &, |, ^, ~, <<, >>
  • Membership: in, not in
  • Identity: is, is not
  • Special / ternary: a if cond else b

6.2 Arithmetic operators

Used for numeric calculations.

# addition, subtraction, multiplication, division
a = 7 + 3        # 10
b = 7 - 3        # 4
c = 7 * 3        # 21
d = 7 / 3        # 2.3333333333333335 (always float in Python 3)

# floor division (quotient rounded down)
e = 7 // 3       # 2
f = -7 // 3      # -3  (floor: rounds toward -infinity)

# modulo (remainder)
r = 7 % 3        # 1
# note for negatives: -7 % 3 == 2 (result has same sign as divisor)

# exponentiation
p = 2 ** 3       # 8

Notes: floor division (//) behaves differently with negative numbers (it floors), and modulo with negatives follows the rule that a == (a // b) * b + (a % b). For floating-point arithmetic be aware of precision issues (see pitfalls).

6.3 Assignment and augmented assignment

# simple assignment
x = 10

# multiple and chained assignment
a = b = c = 0
x, y = 1, 2   # tuple unpacking

# augmented assignment
x += 5   # same as x = x + 5
x *= 2   # same as x = x * 2

Important: For mutable objects augmented assignment may mutate in-place (see section 6.10).

6.4 Comparison operators

1 == 1    # True
1 != 2    # True
3 > 2   # True
2 <= 5  # True

# Chained comparisons are allowed
0 < x <= 10   # equivalent to (0 < x) and (x <= 10)

Equality vs identity: == checks equality (calls __eq__), while is checks object identity (same object in memory). Use is for singletons (e.g., None), but not for value equality.

a = [1,2,3]
b = [1,2,3]
a == b      # True  (same contents)
a is b      # False (different objects)

6.5 Logical operators (and, or, not)

These operate on truthiness and short-circuit.

True and False    # False
True or False     # True
not True          # False

# short-circuit behavior:
def side_effect():
    print("called")
    return True

False and side_effect()  # side_effect() not called (short-circuit)
True or side_effect()    # side_effect() not called (short-circuit)

Return values: and/or return the actual operand (not always boolean). This is useful for idioms like value = user_input or default.

"" or "fallback"    # "fallback"
"hello" and "world"    # "world" (first falsy or last truthy)

6.6 Bitwise operators

Operate on binary representations (integers only).

# bitwise AND, OR, XOR
5 & 3   # 1   (0b101 & 0b011 = 0b001)
5 | 3      # 7   (0b101 | 0b011 = 0b111)
5 ^ 3      # 6   (0b101 ^ 0b011 = 0b110)

# bitwise NOT (two's complement representation)
~5         # -6

# shifts
1 << 3  # 8   (multiply by 2**3)
8 >> 2  # 2   (floor divide by 2**2)

Use bin(x) to inspect binary form: bin(5) == '0b101'.

6.7 Membership operators: in / not in

"a" in "apple"          # True
3 in [1,2,3]               # True
"key" in {"key": 1}        # True  (checks keys in dictionaries)
4 not in (1,2,3)           # True

6.8 Identity operators: is / is not

is checks whether two references point to the same object (identity). For checking None use is None not == None.

a = None
a is None      # True

# caution: small ints and short strings may be interned by Python,
# so 'is' might appear to work for them but it's not the correct test:
x = 256
y = 256
x is y         # True on many implementations (interned)
x == y         # True

6.9 Operator precedence (important)

Operators have an order of evaluation. Use parentheses to make intentions explicit. Below is a compact precedence list (higher means evaluated first):

  1. ** (exponentiation, right-to-left)
  2. Unary +x, -x, ~x
  3. *, /, //, %
  4. +, -
  5. <<, >>
  6. &
  7. ^
  8. |
  9. Comparisons (==, !=, <, >, <=, >=, is, in)
  10. not
  11. and
  12. or
  13. Assignment operators (=, +=, ...)

When in doubt, add parentheses:

result = (a + b) * c

6.10 Augmented assignment & mutability (subtle behaviour)

Augmented assignment (+=, *=, ...) may modify mutable objects in-place, while regular assignment creates a new object.

lst = [1,2]
id_before = id(lst)
lst += [3]          # modifies list in-place
id_after = id(lst)
id_before == id_after   # True

lst2 = [1,2]
lst2 = lst2 + [3]   # creates new list object
# id(lst2) will be different

Takeaway: when using augmented assignment with mutables (lists, dicts, sets) be aware that the object may be changed in-place which affects other references to it.

6.11 Operator overloading (custom behaviour)

Python classes can define special methods (dunder methods) to customize operator behaviour: __add__, __eq__, __lt__, etc.

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1,2)
v2 = Vector(3,4)
print(v1 + v2)   # Vector(4, 6)

6.12 Ternary (conditional) operator

status = "adult" if age >= 18 else "minor"

This is a concise alternative to a short if/else.

6.13 The operator module

For functional-style code you can use the operator module (handy as key functions for sorting, etc.).

import operator
pairs = [(1, 'one'), (3, 'three'), (2, 'two')]
# sort by first item (index 0)
pairs.sort(key=operator.itemgetter(0))
# function equivalent
pairs.sort(key=lambda x: x[0])

6.14 Practical examples & common pitfalls

  • Float precision: 0.1 + 0.2 != 0.3 due to binary floating-point. Use math.isclose() for comparisons.
  • Division: / always returns float. Use // for integer (floor) division.
  • is vs ==: Use == for value equality and is only when you need identity (e.g., is None).
  • Membership performance: x in list is O(n), but x in set / x in dict is on average O(1).
  • Bitwise vs logical: Bitwise operators operate on integers (or booleans seen as ints); do not confuse them with logical operators (& vs and).
  • Chained comparisons: 1 < x < 5 is elegant and efficient — it does not evaluate x twice.

6.15 Quick cheatsheet

OperatorMeaningExample
+Add2 + 3 # 5
-Subtract / unary minus-5
*Multiply / repeat (sequences)[1]*3 # [1,1,1]
/True division5 / 2 # 2.5
//Floor division5 // 2 # 2
%Modulo7 % 3 # 1
**Exponent2 ** 3 # 8
==, !=Equality / inequality1 == 1 # True
&, |, ^Bitwise AND/OR/XOR5 & 3 # 1
and, or, notLogicalTrue and False # False
in, not inMembership'a' in 'apple' # True
is, is notIdentitya is None

6.16 Summary

Operators are fundamental. Learn the categories, understand precedence, be aware of mutability and short-circuiting, and always prefer readable expressions with parentheses where there is any risk of ambiguity. For custom types implement the appropriate dunder methods to get intuitive operator behaviour.

7. Input & Output (I/O) in Python — full details

Input/Output (I/O) covers how your program receives data and how it shows results. This section focuses on console/terminal I/O (keyboard & screen), standard streams, formatting output, reading from pipes/STDIN, and best practices for interactive and script use.

7.1 The basic print()

print() is the primary way to send text to standard output.

print("Hello, world!")               # simple

# print multiple values (space-separated by default)
x, y = 10, 20
print("x =", x, "y =", y)

# change separator and line end
print("a", "b", "c", sep="|")         # a|b|c
print("No newline", end="")           # suppress newline
print(" next")                        # continues same line

7.2 Advanced print() options

# print to a different file-like object (stderr)
import sys
print("This is an error", file=sys.stderr)

# flush output immediately (useful for progress updates)
print("Processing...", flush=True)

7.3 Formatted output — three common approaches

Formatting makes output readable and lets you control numeric precision, padding and alignment.

Old-style (%) formatting (legacy)

name = "Amit"
score = 95.678
print("Name: %s, Score: %.2f" % (name, score))

str.format() (flexible)

print("Name: {}, Score: {:.2f}".format(name, score))
# named fields
print("Name: {n}, Score: {s:.1f}".format(n=name, s=score))

f-strings (Python 3.6+) — recommended

print(f"Name: {name}, Score: {score:.2f}")
# width, alignment, fill and thousand separator
num = 12345.6789
print(f"{num:,.2f}")            # 12,345.68
print(f"{name:>10}")            # right aligned in width 10
print(f"{name:*^10}")           # centered width 10, '*' fill

7.4 Reading console input with input()

input(prompt) reads one line from the user (as string). It raises EOFError on EOF.

name = input("Enter your name: ")
age_str = input("Enter age: ")
try:
    age = int(age_str)
except ValueError:
    print("Please enter a valid integer for age")

Common patterns: splitting multiple values in one line:

# user enters: 10 20 30
a, b, c = map(int, input("Enter three numbers: ").split())

7.5 Robust input handling

import sys

try:
    data = input("Enter a number: ")
except EOFError:
    print("No input (EOF). Exiting.")
    sys.exit(0)
except KeyboardInterrupt:
    print("\nCancelled by user.")
    sys.exit(1)

7.6 Standard streams: sys.stdin, sys.stdout, sys.stderr

These let you read/write programmatically and are used for piping and redirection.

import sys

# read full stdin (useful when input is piped)
data = sys.stdin.read()

# iterate lines from stdin (memory efficient)
for line in sys.stdin:
    process(line)

# write directly without newline
sys.stdout.write("No newline")
sys.stdout.flush()

7.7 Binary streams

For byte-level I/O (images, compressed data) use the buffered streams.

import sys

# read bytes from stdin
data_bytes = sys.stdin.buffer.read()

# write bytes to stdout
sys.stdout.buffer.write(b"\x00\x01")
sys.stdout.buffer.flush()

7.8 Reading until EOF / piping example

Useful when your script is part of a shell pipeline.

import sys

# echo program: reads stdin, writes uppercase to stdout
for line in sys.stdin:
    sys.stdout.write(line.upper())

7.9 Progress updates on same line

Use end='\r' and flush=True to overwrite the same console line.

import time, sys

for i in range(1, 6):
    print(f"Progress: {i}/5", end="\r", flush=True)
    time.sleep(0.5)
print()  # finish with newline

7.10 Hidden input (passwords)

from getpass import getpass

pwd = getpass("Enter password: ")
# getpass does not echo characters to the terminal

7.11 Printing to files and logging

Prefer logging for production. For quick scripts you can print to a file-like object.

with open("out.log", "a", encoding="utf-8") as logfile:
    print("An event occurred", file=logfile)

# or use logging module (recommended)
import logging
logging.basicConfig(level=logging.INFO)
logging.info("Started")

7.12 Interactive vs non-interactive scripts

Interactive programs use input() and expect a terminal. Non-interactive scripts should read from sys.stdin (so they work with pipes) and avoid prompting unless sys.stdin.isatty() is True.

import sys

if sys.stdin.isatty():
    name = input("Name: ")
else:
    # data is coming from a pipe or file
    data = sys.stdin.read()

7.13 Encoding & terminals

When printing non-ASCII text be aware of terminal encoding. Modern terminals generally support UTF-8. If you get encoding errors, you may use sys.stdout.reconfigure(encoding="utf-8") (Py3.7+) or set environment locale.

7.14 Example: Simple CLI calculator (reads tokens)

# usage: echo "10 + 20" | python calc.py
import sys

expr = sys.stdin.read().strip()
if not expr:
    print("No expression provided.")
else:
    try:
        # WARNING: eval can be dangerous with untrusted input.
        result = eval(expr, {"__builtins__": None}, {})
        print(result)
    except Exception as e:
        print("Error:", e, file=sys.stderr)

7.15 Common pitfalls & tips

  • Don't use eval on untrusted input — it runs arbitrary code.
  • Validate and try/except when converting input (e.g., int(), float()).
  • Prefer for line in sys.stdin for piped input — it's memory efficient.
  • Use print(..., file=sys.stderr) for error messages so they don't mix with normal output streams.
  • Use logging for traceability and different log levels (INFO, WARNING, ERROR).

7.16 Quick cheatsheet

TaskExample
Simple printprint("hello")
Formatted (f-string)print(f"Val={val:.2f}")
Read one lineline = input("prompt: ")
Read piped datadata = sys.stdin.read()
Write to stderrprint("err", file=sys.stderr)
Byte I/Osys.stdin.buffer.read()
Hidden inputpwd = getpass()

7.17 Summary

Console I/O in Python is simple but powerful. Use print() and input() for quick scripts, prefer sys.stdin/sys.stdout for piped data, use buffering/flush control for progress displays, and choose logging for real applications. Always validate input and be careful with operations like eval.

8. Conditional Statements in Python

Conditional statements in Python allow us to execute different blocks of code depending on whether a condition is True or False. These statements are the decision-making backbone of Python programming.

8.1 The if Statement

The if statement is used to check a condition. If the condition evaluates to True, the indented block under if will execute.

x = 10
if x > 5:
    print("x is greater than 5")

8.2 The if-else Statement

The if-else statement provides an alternative path. If the condition is False, the else block executes.

x = 3
if x > 5:
    print("x is greater than 5")
else:
    print("x is less than or equal to 5")

8.3 The if-elif-else Ladder

When there are multiple conditions, we use elif (short for "else if") between if and else.

marks = 85
if marks >= 90:
    print("Grade: A")
elif marks >= 75:
    print("Grade: B")
elif marks >= 50:
    print("Grade: C")
else:
    print("Grade: Fail")

8.4 Nested if Statements

You can place one if statement inside another. This is known as a nested if.

age = 20
if age >= 18:
    if age < 60:
        print("You are an adult but not a senior citizen")
    else:
        print("You are a senior citizen")
else:
    print("You are under 18")

8.5 Short Hand if

Python allows writing if statements in a single line.

x = 7
if x > 5: print("x is greater than 5")

8.6 Short Hand if-else (Ternary Operator)

A shorthand way to write if-else in one line.

a = 10
b = 20

print("a is greater") if a > b else print("b is greater")

8.7 Logical Operators with Conditions

Conditions can be combined using and, or, and not.

x = 15
if x > 10 and x < 20:
    print("x is between 10 and 20")
x = 5
if x < 10 or x == 20:
    print("Condition is True")
x = False
if not x:
    print("x is False")

8.8 Using Conditions with Membership Operators

Membership operators (in, not in) are useful for checking values inside sequences.

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

if "banana" in fruits:
    print("Banana is available")

if "mango" not in fruits:
    print("Mango is not available")

8.9 Using Conditions with Identity Operators

Identity operators (is, is not) check whether two variables refer to the same object.

a = [1, 2, 3]
b = a
c = [1, 2, 3]

print(a is b)   # True (same object)
print(a is c)   # False (different objects with same values)
print(a is not c) # True

8.10 Example: Even or Odd

num = 7
if num % 2 == 0:
    print("Even number")
else:
    print("Odd number")

8.11 Example: Checking Leap Year

year = 2024
if (year % 400 == 0) or (year % 4 == 0 and year % 100 != 0):
    print("Leap Year")
else:
    print("Not a Leap Year")

8.12 Summary of Conditional Statements

Statement Usage Example
if Executes if condition is True if x > 5:
if-else Chooses between two paths if x>5 else
if-elif-else Multiple conditions elif x==10:
nested if if inside another if if a>0: if a%2==0:
short hand if One line if if x>5: print()
ternary operator Short if-else print("A") if a>b else print("B")

9. Loops in Python

Loops in Python allow us to repeat a block of code multiple times. They are essential when you want to perform repetitive tasks efficiently. Python mainly provides two types of loops: for loop and while loop.

9.1 The for Loop

A for loop is used to iterate over a sequence such as a list, tuple, dictionary, set, or string.

fruits = ["apple", "banana", "cherry"]
for fruit in fruits:
    print(fruit)

9.2 The range() Function

The range() function is commonly used with for loops to generate a sequence of numbers.

for i in range(5):
    print(i)   # prints 0 to 4
for i in range(2, 10, 2):
    print(i)   # prints even numbers 2, 4, 6, 8

9.3 The while Loop

A while loop executes as long as a given condition is True.

x = 1
while x <= 5:
    print(x)
    x += 1

9.4 The break Statement

The break statement terminates the loop immediately, even if the condition is still True.

for i in range(10):
    if i == 5:
        break
    print(i)

9.5 The continue Statement

The continue statement skips the current iteration and moves to the next iteration of the loop.

for i in range(6):
    if i == 3:
        continue
    print(i)

9.6 The else Block with Loops

A loop can have an else block. The else part executes only if the loop is not terminated by a break.

for i in range(5):
    print(i)
else:
    print("Loop finished!")

9.7 Nested Loops

You can place one loop inside another. This is useful for working with matrices or patterns.

for i in range(3):       # outer loop
    for j in range(2):   # inner loop
        print(f"i={i}, j={j}")

9.8 Looping Through a String

for ch in "Python":
    print(ch)

9.9 Looping Through a Dictionary

student = {"name": "Amit", "age": 21, "course": "Python"}

for key, value in student.items():
    print(key, ":", value)

9.10 Infinite Loops

A loop that never ends is called an infinite loop. Use with caution and always include a break condition.

while True:
    print("This is an infinite loop")
    break

9.11 Common Loop Patterns

  • Summing numbers:
  • total = 0
    for i in range(1, 6):
        total += i
    print("Sum:", total)
  • Factorial using while loop:
  • num = 5
    fact = 1
    while num > 0:
        fact *= num
        num -= 1
    print("Factorial:", fact)

9.12 Practical Example: Multiplication Table

num = 7
for i in range(1, 11):
    print(f"{num} x {i} = {num * i}")

9.13 Summary of Loops

Concept Description Example
for loop Iterates over a sequence for i in range(5):
while loop Repeats while condition is True while x <= 5:
break Stops the loop immediately if i==5: break
continue Skips current iteration if i==3: continue
else with loop Executes if loop ends normally for i in range(3): ... else:
nested loops Loop inside another loop for i in ... for j in ...

10. Functions in Python

A function in Python is a block of organized, reusable code that performs a specific task. Functions help make code modular, easier to read, maintain, and debug.

10.1 Defining a Function

A function is defined using the def keyword followed by the function name, parentheses, and a colon. Inside the block, we write the function body.

def greet():
    print("Hello, welcome to Python!")

10.2 Calling a Function

To execute a function, simply use its name followed by parentheses.

greet()

10.3 Function with Parameters

Parameters allow us to pass values into a function. These values are called arguments when passed during the call.

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

greet("Amit")
greet("Priya")

10.4 Function with Return Value

Functions can return values using the return statement.

def add(a, b):
    return a + b

result = add(5, 3)
print("Sum:", result)

10.5 Default Parameters

You can assign default values to parameters. If no argument is provided, the default is used.

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

greet()
greet("Suman")

10.6 Keyword Arguments

Python allows specifying arguments using parameter names.

def introduce(name, age):
    print(f"My name is {name} and I am {age} years old.")

introduce(age=25, name="Rahul")

10.7 Variable-Length Arguments

Sometimes, you may not know in advance how many arguments will be passed. Python supports two types:

  • *args → Non-keyword variable arguments
  • **kwargs → Keyword variable arguments
# Using *args
def add_numbers(*args):
    return sum(args)

print(add_numbers(2, 4, 6, 8))
# Using **kwargs
def display_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

display_info(name="Ankit", age=30, city="Delhi")

10.8 Nested Functions

You can define a function inside another function.

def outer():
    print("This is outer function.")

    def inner():
        print("This is inner function.")

    inner()

outer()

10.9 Lambda Functions (Anonymous Functions)

Lambda functions are small, anonymous functions defined with the lambda keyword.

square = lambda x: x * x
print(square(5))
add = lambda a, b: a + b
print(add(3, 7))

10.10 Recursion

A function that calls itself is called a recursive function.

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n - 1)

print("Factorial of 5:", factorial(5))

10.11 The pass Statement

If a function has no implementation yet, you can use pass as a placeholder.

def future_function():
    pass

10.12 Docstrings

Functions can have documentation strings (docstrings) to describe what the function does.

def greet(name):
    """This function greets the person passed as a parameter."""
    print("Hello,", name)

print(greet.__doc__)

10.13 Built-in Functions vs User-Defined Functions

Python provides many built-in functions like len(), print(), type(), etc. User-defined functions are the ones you create yourself.

10.14 Practical Example: Calculator Function

def calculator(a, b, operation):
    if operation == "add":
        return a + b
    elif operation == "subtract":
        return a - b
    elif operation == "multiply":
        return a * b
    elif operation == "divide":
        return a / b
    else:
        return "Invalid operation"

print(calculator(10, 5, "add"))
print(calculator(10, 5, "multiply"))

10.15 Summary of Functions

Concept Description Example
Defining Create function with def def greet():
Calling Execute function greet()
Parameters Pass values to function def add(a,b):
Return Send back a value return a+b
*args Variable non-keyword args def f(*args):
**kwargs Variable keyword args def f(**kwargs):
Lambda Anonymous small function lambda x: x*x
Recursion Function calling itself factorial(n)

11. Lists in Python

A list in Python is a collection that can store multiple items in a single variable. Lists are ordered, mutable (changeable), and allow duplicate elements. They are defined using square brackets [].

11.1 Creating Lists

# Empty list
my_list = []

# List of integers
numbers = [1, 2, 3, 4, 5]

# Mixed data types
mixed = [10, "apple", 3.14, True]

11.2 Accessing List Elements

Use index numbers (starting from 0) to access elements.

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

print(fruits[0])   # apple
print(fruits[2])   # cherry

11.3 Negative Indexing

Negative indices count from the end (-1 is last element).

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

print(fruits[-1])  # cherry
print(fruits[-2])  # banana

11.4 List Slicing

Use slicing to get a sub-list.

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

print(fruits[1:4])   # ['banana', 'cherry', 'date']
print(fruits[:3])    # ['apple', 'banana', 'cherry']
print(fruits[2:])    # ['cherry', 'date', 'mango']

11.5 Modifying List Elements

fruits = ["apple", "banana", "cherry"]
fruits[1] = "blueberry"
print(fruits)   # ['apple', 'blueberry', 'cherry']

11.6 Adding Elements

  • append() → Adds an item at the end.
  • insert() → Inserts at a specific position.
  • extend() → Adds elements from another list.
fruits = ["apple", "banana"]

fruits.append("cherry")
fruits.insert(1, "orange")
fruits.extend(["mango", "grape"])
print(fruits)

11.7 Removing Elements

  • remove() → Removes by value.
  • pop() → Removes by index (default last).
  • del → Deletes by index.
  • clear() → Removes all items.
fruits = ["apple", "banana", "cherry", "date"]

fruits.remove("banana")
fruits.pop(1)
del fruits[0]
fruits.clear()
print(fruits)   # []

11.8 Iterating Over a List

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

for fruit in fruits:
    print(fruit)

11.9 Checking Membership

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

if "banana" in fruits:
    print("Banana is present")

11.10 Sorting and Reversing

numbers = [5, 2, 9, 1, 7]

numbers.sort()
print(numbers)   # [1, 2, 5, 7, 9]

numbers.sort(reverse=True)
print(numbers)   # [9, 7, 5, 2, 1]

numbers.reverse()
print(numbers)   # reversed order

11.11 Copying Lists

list1 = [1, 2, 3]
list2 = list1.copy()
list3 = list(list1)
print(list2, list3)

11.12 List Comprehensions

List comprehensions provide a concise way to create lists using a single line of code.

numbers = [x for x in range(10)]
print(numbers)

squares = [x*x for x in range(6)]
print(squares)

11.13 Nested Lists

Lists can contain other lists.

matrix = [[1, 2], [3, 4], [5, 6]]
print(matrix[1][0])   # 3

11.14 Useful List Functions

numbers = [10, 20, 30, 40]

print(len(numbers))    # length
print(max(numbers))    # maximum
print(min(numbers))    # minimum
print(sum(numbers))    # sum

11.15 Practical Example: Removing Duplicates

numbers = [1, 2, 2, 3, 4, 4, 5]
unique = list(set(numbers))
print(unique)

11.16 Summary of Lists

Operation Description Example
append() Add at end fruits.append("mango")
insert() Add at position fruits.insert(1, "orange")
extend() Add multiple items fruits.extend(["grape","melon"])
remove() Remove by value fruits.remove("apple")
pop() Remove by index fruits.pop(2)
clear() Remove all items fruits.clear()
sort() Sort list numbers.sort()
reverse() Reverse list numbers.reverse()
copy() Copy list new_list = old.copy()
comprehension Quick list creation [x*x for x in range(5)]

12. Tuples in Python (Full details)

A tuple is an ordered, immutable sequence type in Python. Tuples are similar to lists but cannot be changed after creation (no item assignment, no append/pop). Because of immutability, tuples are often used for fixed collections of items (like coordinates), as dictionary keys, or to return multiple values from functions.

12.1 Creating tuples

# using parentheses
t1 = (1, 2, 3)

# parentheses are optional for simple packing
t2 = 4, 5, 6

# empty tuple
t0 = ()

# single-element tuple (note trailing comma)
single = (5,)
not_a_tuple = (5)   # this is just integer 5

12.2 Accessing elements (indexing & slicing)

t = ("a", "b", "c", "d")

print(t[0])     # 'a'
print(t[-1])    # 'd' (last element)

# slicing returns a new tuple
print(t[1:3])   # ('b', 'c')
print(t[:2])    # ('a', 'b')

12.3 Tuple immutability (what you can and cannot do)

Once created, elements cannot be replaced or removed using list-style methods. You can, however, create new tuples by concatenation or conversion.

t = (1, 2, 3)
# invalid: t[0] = 10   # TypeError

# to 'change' create a new tuple
t = t + (4,)      # (1, 2, 3, 4)
t = (0,) + t      # (0, 1, 2, 3, 4)

12.4 Single-element tuple gotcha

a = (10)    # int
b = (10,)   # tuple with one element
print(type(a), type(b))

12.5 Tuple methods

Tuples have only a few methods because they're immutable.

t = (1,2,2,3)
print(t.count(2))   # 2  (number of occurrences)
print(t.index(3))   # 3  (index of first occurrence)

12.6 Packing and unpacking

Packing packs multiple values into a tuple; unpacking assigns tuple elements to variables.

# packing
pair = ("John", 30)

# unpacking
name, age = pair
print(name)  # John
print(age)   # 30

# swapping variables (idiomatic Python)
a, b = 1, 2
a, b = b, a

12.7 Extended unpacking (starred expressions)

nums = (1,2,3,4,5)
first, *middle, last = nums
print(first)   # 1
print(middle)  # [2,3,4]  (note: starred part becomes a list)
print(last)    # 5

12.8 Returning multiple values from functions

Functions often return tuples implicitly — convenient for multiple results.

def min_max(seq):
    return min(seq), max(seq)

mn, mx = min_max([10, 2, 9, 4])
print(mn, mx)  # 2 9

12.9 Iteration & common patterns

t = (10, 20, 30)
for item in t:
    print(item)

# enumerating
for idx, val in enumerate(t):
    print(idx, val)

12.10 Converting between tuples and lists

lst = [1, 2, 3]
t = tuple(lst)   # (1, 2, 3)

t2 = (4, 5)
lst2 = list(t2)  # [4, 5]

12.11 Concatenation and repetition

(1,2) + (3,4)        # (1, 2, 3, 4)
(0,) * 3               # (0, 0, 0)

12.12 Tuples as dictionary keys and set elements

Tuples (containing only hashable items) are hashable themselves and can be used as keys.

coords = (10, 20)
d = { coords: "Location A" }
print(d[(10, 20)])

12.13 Immutability caveat: mutable elements inside a tuple

A tuple is immutable, but it may contain mutable objects which can be changed.

t = ([1,2], 3)
t[0].append(4)   # allowed — modifies the list inside the tuple
print(t)         # ([1, 2, 4], 3)

12.14 Memory & performance notes

Tuples typically use slightly less memory than lists and are marginally faster for iteration when values are fixed. They are a good choice for heterogeneous, read-only data. For large mutable sequences prefer lists.

import sys
l = [1,2,3,4,5]
t = (1,2,3,4,5)
print(sys.getsizeof(l), sys.getsizeof(t))  # sizes may differ by implementation

12.15 Named tuples (readability & immutability)

The collections.namedtuple creates tuple-like objects with named fields.

from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p = Point(10, 20)
print(p.x, p.y)   # 10 20
print(isinstance(p, tuple))  # True

12.16 Using tuple() constructor and generator expressions

t = tuple([x*x for x in range(5)])   # from list comprehension
t2 = tuple(x*x for x in range(5))          # from generator expression

12.17 Comparison of tuple vs list (when to use)

  • Use tuple for heterogeneous, fixed-size, or read-only collections (e.g., coordinates, records).
  • Use list when you need to modify the collection (add/remove/replace items).
  • Tuples can be dictionary keys — lists cannot.

12.18 Practical examples

# Function returning multiple results
def divide_and_remainder(a, b):
    return a // b, a % b

quot, rem = divide_and_remainder(17, 5)
print(quot, rem)   # 3 2

# Use tuple for constant configuration
DB_CONFIG = ("db.example.com", 5432, "user", "pass")

12.19 Common pitfalls & tips

  • Remember the trailing comma for single-element tuple: (42,).
  • Do not expect tuple methods like append() or pop() — they don't exist.
  • If you need to change many items, convert to a list, mutate, then convert back: t = tuple(list(t)).
  • Be careful when tuples contain mutable objects — their contents can still change.

12.20 Quick cheatsheet

OperationExample
Createt = (1,2,3) or t = 1,2,3
Single elementt = (5,)
Unpacka,b = (1,2)
Slicet[1:3]
Concat / repeatt + (4,) ; t * 3
Converttuple(list) ; list(tuple)
Methodscount(), index()

12.21 Summary

Tuples are lightweight, immutable sequences ideal for fixed collections and keys in mappings. They support packing/unpacking, slicing, and have minimal methods. Use them when immutability, small memory footprint, or hashability is desired.

13. Sets in Python (Full Details)

A set in Python is an unordered, mutable collection of unique elements. Sets are useful when you want to store multiple items without duplicates and perform mathematical set operations like union, intersection, and difference.

13.1 Creating Sets

# empty set (must use set(), not {})
s = set()

# from a list
s1 = set([1, 2, 3, 4, 4])   # {1, 2, 3, 4}

# directly using curly braces
s2 = {1, 2, 3, 4}

# mixed data types
s3 = {10, "apple", 3.14, True}

print(s1, s2, s3)

13.2 Properties of Sets

  • Unordered → no indexing or slicing allowed.
  • Unique → duplicate elements are automatically removed.
  • Mutable → items can be added or removed, but only immutable items (numbers, strings, tuples) can be elements.

13.3 Adding Elements

  • add() → Add a single item.
  • update() → Add multiple items (from list, set, tuple).
fruits = {"apple", "banana"}
fruits.add("cherry")
fruits.update(["mango", "grape"])
print(fruits)

13.4 Removing Elements

  • remove() → Removes item; error if not present.
  • discard() → Removes item; no error if not present.
  • pop() → Removes and returns a random item.
  • clear() → Empties the set.
fruits = {"apple", "banana", "cherry"}
fruits.remove("banana")
fruits.discard("pear")    # no error
print(fruits)

item = fruits.pop()
print("Removed:", item)

fruits.clear()
print(fruits)   # set()

13.5 Iterating Over a Set

fruits = {"apple", "banana", "cherry"}
for f in fruits:
    print(f)

13.6 Membership Test

fruits = {"apple", "banana", "cherry"}
print("apple" in fruits)   # True
print("mango" not in fruits)   # True

13.7 Set Operations (Mathematical)

Python sets support union, intersection, difference, and symmetric difference.

A = {1, 2, 3, 4}
B = {3, 4, 5, 6}

print(A | B)   # Union → {1,2,3,4,5,6}
print(A & B)   # Intersection → {3,4}
print(A - B)   # Difference → {1,2}
print(B - A)   # Difference → {5,6}
print(A ^ B)   # Symmetric difference → {1,2,5,6}

13.8 Set Methods for Operations

A = {1,2,3}
B = {3,4,5}

print(A.union(B))
print(A.intersection(B))
print(A.difference(B))
print(A.symmetric_difference(B))

13.9 Subset and Superset

A = {1,2,3}
B = {1,2,3,4,5}

print(A.issubset(B))   # True
print(B.issuperset(A)) # True

13.10 Disjoint Sets

Two sets are disjoint if they have no elements in common.

A = {1,2,3}
B = {4,5,6}

print(A.isdisjoint(B))   # True

13.11 Frozensets (Immutable Sets)

A frozenset is an immutable version of a set — elements cannot be added or removed.

A = frozenset([1,2,3,4])
print(A)

# frozensets can be used as dictionary keys
d = {A: "numbers"}
print(d)

13.12 Set Comprehensions

squares = {x*x for x in range(6)}
print(squares)   # {0, 1, 4, 9, 16, 25}

13.13 Removing Duplicates from a List using Set

numbers = [1,2,2,3,4,4,5]
unique = list(set(numbers))
print(unique)

13.14 Practical Examples

# Finding unique words in a sentence
sentence = "python is great and python is easy"
words = set(sentence.split())
print(words)

# Common friends example
friends_A = {"John", "Alice", "Bob"}
friends_B = {"Alice", "David", "Bob"}
common = friends_A & friends_B
print("Common friends:", common)

13.15 Common Pitfalls

  • Empty set must be created with set(), not {} (which creates a dictionary).
  • Sets are unordered, so you cannot index them (e.g., set[0] → error).
  • Only immutable elements (numbers, strings, tuples) can be set members. Lists and dictionaries cannot.

13.16 Useful Built-in Functions with Sets

A = {1,2,3,4,5}

print(len(A))   # 5
print(max(A))   # 5
print(min(A))   # 1
print(sum(A))   # 15

13.17 Performance Notes

Sets use hash tables internally, so membership tests (in and not in) are much faster than lists, especially for large collections.

13.18 Quick Cheatsheet

OperationExample
Create{1,2,3} or set([1,2,3])
Adds.add(4)
Updates.update([5,6])
Removes.remove(2)
Discards.discard(10)
UnionA | B
IntersectionA & B
DifferenceA - B
Symmetric DifferenceA ^ B
SubsetA.issubset(B)
SupersetA.issuperset(B)
DisjointA.isdisjoint(B)

13.19 Summary

Sets are powerful for handling unique elements and performing mathematical operations efficiently. Use them when you need fast membership tests, unique collections, or set algebra operations.

14. Dictionaries in Python (Full details)

A dictionary (or dict) is Python’s built-in mapping type: an unordered (in older versions) — now insertion-ordered — collection of key:value pairs. Dictionaries are mutable, fast for lookups (average O(1)), and extremely useful for representing structured data.

14.1 Creating dictionaries

# literal syntax
d1 = {"name": "Amit", "age": 30, "city": "Delhi"}

# using dict() constructor
d2 = dict(name="Rina", age=25)

# from pairs (list of tuples)
d3 = dict([("a", 1), ("b", 2)])

# empty dict
empty = {}

14.2 Accessing values

Access by key using square brackets or get() which is safer for missing keys.

print(d1["name"])         # Amit

# safer access (returns None if missing)
print(d1.get("salary"))      # None

# with default
print(d1.get("salary", 0))   # 0

14.3 Adding & updating entries

d = {}
d["name"] = "Asha"           # add
d["age"] = 28                # add
d["age"] = 29                # update

# update many at once
d.update({"city": "Mumbai", "role": "engineer"})

14.4 Removing entries

14.5 Iterating dictionaries

Common iteration patterns:

for key in d:            # iterates keys
    print(key, d[key])

for key in d.keys():         # explicit keys view
    print(key)

for value in d.values():     # values view
    print(value)

for key, value in d.items(): # key-value pairs
    print(key, value)

Note: keys(), values(), and items() return dynamic view objects — they reflect changes to the dict.

14.6 Membership tests

"name" in d            # True if key exists
"value" in d.values()      # checks values (less efficient)

14.7 Dictionary comprehensions

14.8 Common useful methods

  • d.get(key, default) — safe access
  • d.setdefault(key, default) — get existing or set default and return it
  • d.update(other) — merge/update
  • d.pop(key[, default]) — remove and return value
  • d.popitem() — remove and return (key, value)
  • d.clear() — remove all items
  • d.copy() — shallow copy
# setdefault example (useful for grouping)
words = ["apple", "banana", "apple", "orange"]
counts = {}
for w in words:
    counts.setdefault(w, 0)
    counts[w] += 1

# using get (more concise)
counts2 = {}
for w in words:
    counts2[w] = counts2.get(w, 0) + 1

14.9 Merging dictionaries

Multiple ways to merge dicts:

# Python 3.9+: | operator (returns new dict)
d3 = d1 | d2

# Python 3.5+ unpacking
d4 = {**d1, **d2}   # keys from d2 override d1

# in-place merge
d1.update(d2)       # modifies d1

14.10 Shallow vs Deep copy

Shallow copy copies the mapping structure but not nested mutable objects. Use copy.deepcopy for deep copying.

import copy
orig = {"a": [1,2], "b": {"x": 10}}
shallow = orig.copy()
deep = copy.deepcopy(orig)

orig["a"].append(3)
print(shallow["a"])   # changed too (shares same list)
print(deep["a"])      # unaffected

14.11 Using immutable keys

Dictionary keys must be hashable (immutable types like strings, numbers, tuples of immutables). Lists and dicts cannot be keys.

valid = {(1,2): "coord", "name": "Amit", 100: "id"}
# invalid: {[1,2]: "no"}  # TypeError: unhashable type: 'list'

14.12 Ordering guarantees

From Python 3.7+, the insertion order of keys is guaranteed. That means iteration follows insertion order (useful for deterministic behavior).

14.13 Nested dictionaries

data = {
    "user1": {"name": "A", "age": 30},
    "user2": {"name": "B", "age": 25}
}
# access nested value
print(data["user1"]["name"])

14.14 defaultdict and Counter (collections)

For common patterns use helpers from collections:

from collections import defaultdict, Counter

# defaultdict avoids manual setdefault/get patterns
dd = defaultdict(list)
dd["a"].append(1)

# Counter is great for counting items
cnt = Counter(["a", "b", "a"])
print(cnt)    # Counter({'a': 2, 'b': 1})

14.15 OrderedDict (legacy use)

collections.OrderedDict preserves insertion order in older Python versions (<3 .7="" 3.7="" behavior="" dict="" for="" is="" mainly="" move_to_end="" needed="" normal="" order="" ordereddict="" p="" preserves="" reordering="" since="" specialised="">

from collections import OrderedDict
od = OrderedDict()
od["a"] = 1
od["b"] = 2
od.move_to_end("a")   # move 'a' to the end

14.16 JSON and dicts

Dictionaries map naturally to JSON objects. Use json.dump/json.load for IO.

import json
with open("data.json", "w", encoding="utf-8") as f:
    json.dump(d1, f, ensure_ascii=False, indent=2)

with open("data.json", "r", encoding="utf-8") as f:
    d_loaded = json.load(f)

14.17 Sorting dictionaries

Dicts themselves are unordered containers of key:value, but you can create sorted views or new dicts ordered by key or value:

# sort by keys
for k in sorted(d.keys()):
    print(k, d[k])

# sort by values (descending) and create ordered dict
sorted_by_value = dict(sorted(d.items(), key=lambda kv: kv[1], reverse=True))

14.18 Performance notes

  • Average lookup, insertion, and deletion are O(1).
  • Be mindful of memory: dicts have overhead (hash table).
  • Use appropriate key types (immutable, small) for best performance.

14.19 Practical examples

Counting words (Counter)

from collections import Counter
text = "this is a sample this is a test"
words = text.split()
counts = Counter(words)
print(counts.most_common(3))

Group items by key

items = [("fruit","apple"), ("veg","carrot"), ("fruit","banana")]
grouped = {}
for k, v in items:
    grouped.setdefault(k, []).append(v)
print(grouped)

14.20 Common pitfalls & tips

  • Don’t use mutable objects (lists, dicts) as keys — they’re unhashable.
  • Use get() to avoid KeyError.
  • When merging dicts, be clear whether you want a new dict or to update in-place.
  • Remember shallow copy semantics when dict values are mutable.
  • Prefer Counter / defaultdict for counting & grouping tasks — less boilerplate and faster.

14.21 Quick cheatsheet

TaskExample
Created = {"a":1}
Access (safe)v = d.get("a", default)
Set / Updated["a"]=2 ; d.update({"b":3})
Removed.pop("a", None) ; del d["b"]
Iteratefor k,v in d.items(): ...
Copyd2 = d.copy() ; deep = copy.deepcopy(d)
Merged3 = d1 | d2 # py3.9+
Comprehension{k: f(k) for k in keys}

14.22 Summary

Dictionaries are the go-to structure for mapping keys to values. Learn the common idioms: safe access with get(), grouping with setdefault() or defaultdict, counting with Counter, merging with | or update(), and mind shallow vs deep copies when values are mutable. For JSON-like data, dicts and the json module are a natural fit.

15. Strings in Python (Full Details)

A string in Python is a sequence of Unicode characters used to represent text. Strings are immutable, which means once created, they cannot be changed. Any operation that modifies a string actually creates a new one.

15.1 Creating Strings

# single quotes
s1 = 'Hello'

# double quotes
s2 = "Hello World"

# triple quotes for multi-line
s3 = '''This is
a multi-line
string'''

# empty string
s4 = ""

15.2 String Indexing and Slicing

Strings are sequences, so you can access characters by index (0-based) and extract substrings with slicing.

text = "Python"

print(text[0])      # P
print(text[-1])     # n (last character)
print(text[0:4])    # Pyth (up to index 3)
print(text[:3])     # Pyt (from start)
print(text[3:])     # hon (till end)
print(text[::-1])   # nohtyP (reverse)

15.3 String Concatenation and Repetition

a = "Hello"
b = "World"

# concatenation
print(a + " " + b)   # Hello World

# repetition
print(a * 3)         # HelloHelloHello

15.4 String Formatting

Three main approaches exist for string formatting:

15.4.1 Old-style (%) formatting

name = "Ravi"
age = 25
print("My name is %s and I am %d years old" % (name, age))

15.4.2 str.format()

print("My name is {} and I am {} years old".format(name, age))
print("Name: {0}, Age: {1}".format(name, age))
print("Age: {age}, Name: {name}".format(name="Ravi", age=25))

15.4.3 f-strings (Python 3.6+)

print(f"My name is {name} and I am {age} years old")
print(f"Next year, age will be {age + 1}")

15.5 Useful String Methods

  • len(s) → length
  • s.lower(), s.upper(), s.title(), s.capitalize()
  • s.strip(), s.lstrip(), s.rstrip()
  • s.startswith(prefix), s.endswith(suffix)
  • s.find(sub), s.rfind(sub), s.index(sub)
  • s.replace(old, new)
  • s.split(sep), s.rsplit(sep), s.join(iterable)
  • s.isalpha(), s.isdigit(), s.isalnum(), s.isspace()
  • s.zfill(width), s.center(width, char), s.ljust(), s.rjust()
text = "  Python Basics  "
print(len(text))             # 16
print(text.strip())          # 'Python Basics'
print(text.lower())          # '  python basics  '
print(text.upper())          # '  PYTHON BASICS  '

print("abc123".isalnum())    # True
print("123".isdigit())       # True
print("abc".isalpha())       # True

15.6 Escape Characters

Escape characters allow you to include special characters inside strings.

text = "Line1\nLine2"   # newline
print(text)

path = "C:\\Users\\Admin"  # backslash
quote = "He said, \"Python is great!\""
raw = r"C:\Users\Admin"    # raw string (ignores escapes)

15.7 String Iteration

for char in "Python":
    print(char)

15.8 Membership Tests

text = "hello world"
print("hello" in text)    # True
print("bye" not in text)  # True

15.9 Splitting and Joining

sentence = "Python is easy to learn"
words = sentence.split()     # ['Python', 'is', 'easy', 'to', 'learn']
print(words)

joined = "-".join(words)     # 'Python-is-easy-to-learn'
print(joined)

15.10 Advanced: String Templates

Using string.Template for safe substitutions (useful with user input).

from string import Template
t = Template("Hello, $name!")
print(t.substitute(name="Ravi"))

15.11 Unicode and Encoding

text = "नमस्ते"
print(len(text))          # number of Unicode characters
print(text.encode("utf-8"))  # b'\xe0\xa4\xa8\xe0\xa4...\xe0xa5\x87'

15.12 Common String Algorithms

Check palindrome

s = "madam"
print(s == s[::-1])   # True

Count vowels

vowels = "aeiou"
word = "education"
count = sum(1 for ch in word if ch.lower() in vowels)
print(count)

15.13 Formatting Numbers in Strings

pi = 3.14159265
print(f"{pi:.2f}")     # 3.14
print(f"{1000:,}")     # 1,000

15.14 String Comparisons

Strings are compared lexicographically (like dictionary order) using Unicode code points.

print("apple" < "banana")    # True
print("abc" == "ABC")        # False
print("abc".lower() == "ABC".lower())  # True

15.15 Summary

Strings are one of the most powerful and commonly used types in Python. Key points:

  • They are immutable sequences of Unicode characters.
  • Support slicing, concatenation, repetition, and iteration.
  • Provide dozens of helpful methods for searching, formatting, and transforming text.
  • Use f-strings for modern, clean formatting.
  • Work seamlessly with Unicode and encodings.

16. File Handling in Python (Full Details)

File handling allows you to store data permanently by reading from and writing to files. Python provides a simple and powerful interface for working with files using the built-in open() function. Files can be text or binary, and operations include creating, reading, writing, and appending.

16.1 Opening and Closing Files

Use open(filename, mode) to work with files. Always close files with close() or preferably use the with statement which automatically closes the file.

# basic syntax
file = open("example.txt", "r")  # open for reading
print(file.read())
file.close()

# recommended approach
with open("example.txt", "r") as f:
    content = f.read()
    print(content)  # file automatically closed after block

16.2 File Modes

ModeDescription
'r'Read (default) — file must exist
'w'Write — creates new or overwrites existing
'a'Append — creates new or writes at end
'x'Create — fails if file exists
'b'Binary mode (e.g., 'rb', 'wb')
't'Text mode (default)
'+'Read and write (e.g., 'r+', 'w+')

16.3 Reading from Files

with open("example.txt", "r") as f:
    data = f.read()        # read entire file as string
    print(data)

with open("example.txt", "r") as f:
    line = f.readline()    # read one line
    print(line)

with open("example.txt", "r") as f:
    lines = f.readlines()  # read all lines as list
    print(lines)

# iterate directly
with open("example.txt", "r") as f:
    for line in f:
        print(line.strip())

16.4 Writing to Files

# write (overwrite)
with open("example.txt", "w") as f:
    f.write("Hello Python\n")
    f.write("File Handling Example")

# append
with open("example.txt", "a") as f:
    f.write("\nAdding new line")

16.5 Working with Binary Files

# write binary
with open("image.png", "rb") as f:
    data = f.read()

with open("copy.png", "wb") as f:
    f.write(data)

16.6 File Object Methods

  • f.read([size]) → read size bytes (or all)
  • f.readline() → read one line
  • f.readlines() → read all lines into list
  • f.write(string) → write string
  • f.writelines(list) → write list of strings
  • f.seek(offset) → move file pointer
  • f.tell() → current file pointer position
  • f.close() → close file
with open("example.txt", "r") as f:
    print(f.read(5))      # read first 5 characters
    print(f.tell())       # pointer position
    f.seek(0)             # move back to start
    print(f.read())

16.7 Checking File Existence

import os

print(os.path.exists("example.txt"))  # True or False

16.8 Working with Directories

import os

print(os.getcwd())          # current directory
os.mkdir("testdir")         # create directory
os.chdir("testdir")         # change directory
os.listdir()                # list files
os.rmdir("testdir")         # remove directory

16.9 Exception Handling with Files

try:
    with open("nofile.txt", "r") as f:
        data = f.read()
except FileNotFoundError:
    print("File not found")
except PermissionError:
    print("No permission")

16.10 Handling CSV Files

import csv

# writing
with open("data.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerow(["Name", "Age"])
    writer.writerow(["Ravi", 25])

# reading
with open("data.csv", "r") as f:
    reader = csv.reader(f)
    for row in reader:
        print(row)

16.11 Handling JSON Files

import json

data = {"name": "Ravi", "age": 25}

# write JSON
with open("data.json", "w") as f:
    json.dump(data, f, indent=2)

# read JSON
with open("data.json", "r") as f:
    loaded = json.load(f)
    print(loaded)

16.12 Context Managers

with statement is best practice for file handling — it ensures files are closed automatically even in case of exceptions.

16.13 Best Practices

  • Always use with open(...) to manage files.
  • Use correct mode (r, w, a, etc.).
  • Handle exceptions for missing files or permission errors.
  • For structured data, prefer csv or json modules.
  • Use binary mode for non-text files (images, videos, etc.).

16.14 Summary

File handling is essential for data storage and processing. Python makes it simple with open(), multiple file modes, and helpful modules like os, csv, and json. Always use with for safe file operations, and apply exception handling for robustness.

17. Exception Handling in Python (Full Details)

Exceptions are errors detected during program execution. Instead of stopping the program abruptly, Python allows you to handle exceptions gracefully using try-except blocks. Exception handling makes programs robust and prevents crashes.

17.1 What is an Exception?

# Example of an exception
print(10 / 0)   # ZeroDivisionError
numbers = [1, 2, 3]
print(numbers[5])  # IndexError

17.2 Basic try-except

try:
    x = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")

17.3 Multiple except blocks

try:
    num = int("abc")
except ValueError:
    print("Invalid number")
except ZeroDivisionError:
    print("Division by zero")

17.4 Catching multiple exceptions in one block

try:
    x = int("abc") / 0
except (ValueError, ZeroDivisionError) as e:
    print("Error:", e)

17.5 Using else with try-except

else block runs if no exception occurs.

try:
    x = int("123")
except ValueError:
    print("Invalid input")
else:
    print("Conversion successful:", x)

17.6 Using finally

finally block always executes (useful for cleanup like closing files).

try:
    f = open("example.txt", "r")
    data = f.read()
except FileNotFoundError:
    print("File not found")
finally:
    print("Closing file")
    try:
        f.close()
    except:
        pass

17.7 Raising exceptions manually

def divide(a, b):
    if b == 0:
        raise ValueError("b cannot be zero")
    return a / b

try:
    result = divide(5, 0)
except ValueError as e:
    print("Error:", e)

17.8 Creating custom exceptions

class NegativeNumberError(Exception):
    pass

def square_root(x):
    if x < 0:
        raise NegativeNumberError("Negative numbers not allowed")
    return x ** 0.5

try:
    print(square_root(-9))
except NegativeNumberError as e:
    print("Custom Exception:", e)

17.9 Common built-in exceptions

  • ZeroDivisionError — divide by zero
  • ValueError — invalid value (e.g., converting "abc" to int)
  • TypeError — operation on incompatible types
  • IndexError — list index out of range
  • KeyError — dictionary key not found
  • FileNotFoundError — file does not exist
  • AttributeError — invalid attribute access
  • ImportError — module import failed
  • StopIteration — iterator exhausted
  • MemoryError — out of memory

17.10 Exception hierarchy

All exceptions inherit from BaseException. Common parent is Exception.

try:
    1 / 0
except Exception as e:
    print("Caught:", type(e).__name__)

17.11 Best Practices

  • Catch specific exceptions rather than using a blanket except:.
  • Use finally for cleanup (closing files, releasing resources).
  • Create custom exceptions for domain-specific errors.
  • Don’t suppress exceptions silently — log them or re-raise when needed.
  • Keep try-except blocks small and focused.

17.12 Example: File Handling with Exceptions

try:
    with open("data.txt", "r") as f:
        content = f.read()
except FileNotFoundError:
    print("File not found")
except PermissionError:
    print("No permission")
else:
    print("File read successfully")
finally:
    print("Execution finished")

17.13 Summary

Exception handling is essential for writing robust applications. Use try-except to catch errors, else for successful execution, and finally for cleanup. Python’s built-in exceptions cover most errors, but you can also define custom ones for specific needs.

18. Modules in Python (Full Details)

A module in Python is simply a file containing Python definitions and statements. Modules help you organize code logically, make it reusable, and easier to maintain. You can use built-in modules, install third-party modules, or create your own.

18.1 Why use Modules?

  • Organize code into separate files for better readability.
  • Reusability – write once, use anywhere.
  • Avoids duplication of code.
  • Provides access to Python’s huge Standard Library.

18.2 Importing a Module

import math
print(math.sqrt(25))   # 5.0
print(math.pi)         # 3.141592653589793

18.3 Importing specific functions or variables

from math import sqrt, pi
print(sqrt(36))  # 6.0
print(pi)

18.4 Import with alias

import math as m
print(m.cos(0))   # 1.0

18.5 Import all symbols

Not recommended because it pollutes the namespace.

from math import *
print(sin(0))

18.6 Common Built-in Modules

  • math – mathematical functions.
  • random – random numbers.
  • datetime – dates and times.
  • os – interacting with the operating system.
  • sys – system-specific parameters.
  • json – JSON parsing and writing.
  • re – regular expressions.
  • collections – advanced data structures.

18.7 Example: Using random module

import random
print(random.randint(1, 100))   # random number between 1 and 100
print(random.choice(["apple", "banana", "cherry"]))

18.8 Example: Using datetime module

import datetime
today = datetime.date.today()
print("Today's date:", today)
print("Year:", today.year)
print("Month:", today.month)

18.9 sys module example

import sys
print("Python version:", sys.version)
print("Command line args:", sys.argv)

18.10 os module example

import os
print("Current working directory:", os.getcwd())
os.mkdir("test_folder")

18.11 Creating Your Own Module

Step 1: Create a Python file my_module.py

# my_module.py
def greet(name):
    return f"Hello, {name}!"

def add(a, b):
    return a + b

Step 2: Import and use it

import my_module
print(my_module.greet("Alice"))
print(my_module.add(10, 5))

18.12 Reloading Modules

If you modify a module while running Python, use importlib.reload.

import importlib
import my_module
importlib.reload(my_module)

18.13 The dir() function

Lists all functions, classes, and variables inside a module.

import math
print(dir(math))

18.14 Installing Third-party Modules

# Install using pip
pip install requests

Then use it:

import requests
response = requests.get("https://api.github.com")
print(response.status_code)

18.15 The __name__ == "__main__" check

When Python runs a file, it sets __name__ to "__main__". This lets you write code that only runs when the file is executed directly, not when imported.

# my_module.py
def greet():
    print("Hello!")

if __name__ == "__main__":
    print("Running as main script")
    greet()

18.16 Summary

  • Modules organize Python code.
  • Import built-in modules (math, os, sys, etc.).
  • Create custom modules for reusability.
  • Install third-party modules with pip.
  • Use __name__ == "__main__" to control execution.

19. Classes & Object-Oriented Programming (OOP) in Python

Python is an object-oriented language, which means it allows the use of classes and objects to model real-world entities. OOP helps in organizing code, reusability, and modularity.

19.1 What is a Class and an Object?

  • Class – A blueprint that defines attributes (variables) and behaviors (methods).
  • Object – An instance of a class.
# Defining a class
class Dog:
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

    def bark(self):
        print(f"{self.name} is barking!")

# Creating objects
dog1 = Dog("Tommy", "Labrador")
dog2 = Dog("Rocky", "Pug")

dog1.bark()
dog2.bark()

19.2 The __init__() method

The __init__ method is a constructor that runs automatically when a new object is created.

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

s1 = Student("Alice", 21)
print(s1.name, s1.age)

19.3 Class Attributes vs Instance Attributes

class Car:
    wheels = 4   # class attribute (same for all objects)

    def __init__(self, brand):
        self.brand = brand  # instance attribute

c1 = Car("Toyota")
c2 = Car("Honda")
print(c1.brand, c1.wheels)
print(c2.brand, c2.wheels)

19.4 Methods in Classes

  • Instance Methods – Work with individual objects.
  • Class Methods – Work with the class itself.
  • Static Methods – Independent utility functions inside a class.
class Example:
    count = 0

    def __init__(self):
        Example.count += 1

    # Instance method
    def show(self):
        print("Instance method called")

    # Class method
    @classmethod
    def total_objects(cls):
        print("Total objects:", cls.count)

    # Static method
    @staticmethod
    def greet():
        print("Hello from static method")

e1 = Example()
e2 = Example()
e1.show()
Example.total_objects()
Example.greet()

19.5 Inheritance

Inheritance allows a class (child) to acquire attributes and methods from another class (parent).

class Animal:
    def speak(self):
        print("Animal makes a sound")

class Dog(Animal):
    def speak(self):
        print("Dog barks")

d = Dog()
d.speak()

19.6 Multiple Inheritance

class A:
    def feature1(self):
        print("Feature 1 from A")

class B:
    def feature2(self):
        print("Feature 2 from B")

class C(A, B):
    pass

c = C()
c.feature1()
c.feature2()

19.7 super() function

Used to call parent class methods inside child class.

class Parent:
    def __init__(self, name):
        self.name = name

class Child(Parent):
    def __init__(self, name, age):
        super().__init__(name)  # call parent constructor
        self.age = age

c = Child("Alice", 10)
print(c.name, c.age)

19.8 Encapsulation

Restricting direct access to variables using private/protected members.

class Account:
    def __init__(self, balance):
        self.__balance = balance   # private variable

    def deposit(self, amount):
        self.__balance += amount

    def get_balance(self):
        return self.__balance

a = Account(1000)
a.deposit(500)
print(a.get_balance())

19.9 Polymorphism

Different classes can define the same method name with different implementations.

class Bird:
    def sound(self):
        print("Bird chirps")

class Dog:
    def sound(self):
        print("Dog barks")

for animal in (Bird(), Dog()):
    animal.sound()

19.10 Operator Overloading

class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

p1 = Point(2, 3)
p2 = Point(4, 5)
p3 = p1 + p2
print(p3.x, p3.y)

19.11 Abstract Classes

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Square(Shape):
    def __init__(self, side):
        self.side = side
    def area(self):
        return self.side * self.side

s = Square(4)
print("Area:", s.area())

19.12 Summary

  • Classes are blueprints, objects are instances.
  • __init__ initializes objects.
  • Supports encapsulation, inheritance, polymorphism.
  • Methods: instance, class, static.
  • Operator overloading and abstract classes enhance OOP design.

20. Advanced Topics in Python (Full Details)

After mastering the basics of Python, it’s important to dive into advanced concepts that make your code more efficient, scalable, and professional. This section covers decorators, generators, iterators, context managers, metaclasses, asynchronous programming, and more.

20.1 Iterators

An iterator is an object that can be iterated upon using __iter__() and __next__().

numbers = [1, 2, 3]
it = iter(numbers)
print(next(it))  # 1
print(next(it))  # 2
print(next(it))  # 3

20.2 Generators

Generators are a simple way to create iterators using yield. They are memory-efficient because they generate values on the fly.

def my_generator():
    for i in range(5):
        yield i

for val in my_generator():
    print(val)

20.3 Generator Expressions

squares = (x*x for x in range(5))
print(next(squares))
print(next(squares))

20.4 Decorators

A decorator is a function that modifies another function without changing its code.

def decorator(func):
    def wrapper():
        print("Before function call")
        func()
        print("After function call")
    return wrapper

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

greet()

20.5 Function Arguments in Decorators

def log_args(func):
    def wrapper(*args, **kwargs):
        print("Arguments:", args, kwargs)
        return func(*args, **kwargs)
    return wrapper

@log_args
def add(a, b):
    return a + b

print(add(3, 4))

20.6 Context Managers

Context managers handle resource allocation and cleanup (e.g., opening/closing files).

with open("example.txt", "w") as f:
    f.write("Hello")

You can also create your own:

class MyContext:
    def __enter__(self):
        print("Entering context")
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print("Exiting context")

with MyContext():
    print("Inside context")

20.7 Lambda Functions

Anonymous single-line functions.

square = lambda x: x * x
print(square(5))

20.8 List, Dict, and Set Comprehensions

# List comprehension
squares = [x*x for x in range(5)]

# Dict comprehension
squares_dict = {x: x*x for x in range(5)}

# Set comprehension
squares_set = {x*x for x in range(5)}

20.9 Metaclasses

Metaclasses define the behavior of classes themselves (advanced OOP feature).

class Meta(type):
    def __new__(cls, name, bases, dct):
        print("Creating class:", name)
        return super().__new__(cls, name, bases, dct)

class MyClass(metaclass=Meta):
    pass

20.10 Asynchronous Programming (async/await)

asyncio is used for concurrent programming with coroutines.

import asyncio

async def task1():
    print("Task 1 started")
    await asyncio.sleep(1)
    print("Task 1 finished")

async def task2():
    print("Task 2 started")
    await asyncio.sleep(2)
    print("Task 2 finished")

async def main():
    await asyncio.gather(task1(), task2())

asyncio.run(main())

20.11 Multithreading & Multiprocessing

import threading

def worker():
    print("Worker thread")

t = threading.Thread(target=worker)
t.start()
t.join()
from multiprocessing import Process

def worker():
    print("Worker process")

p = Process(target=worker)
p.start()
p.join()

20.12 Regular Expressions (Regex)

import re
pattern = r"\d+"  # match digits
text = "My number is 12345"
result = re.findall(pattern, text)
print(result)

20.13 Logging

Instead of using print for debugging, use logging.

import logging
logging.basicConfig(level=logging.INFO)
logging.info("This is an info message")

20.14 Unit Testing

import unittest

def add(a, b):
    return a + b

class TestMath(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)

if __name__ == "__main__":
    unittest.main()

20.15 Virtual Environments

# Create virtual environment
python -m venv myenv

# Activate (Windows)
myenv\Scripts\activate

# Activate (Linux/Mac)
source myenv/bin/activate

20.16 Packaging and Distribution

# setup.py file
from setuptools import setup, find_packages

setup(
    name="mypackage",
    version="0.1",
    packages=find_packages()
)

20.17 Summary

  • Advanced Python covers iterators, generators, and comprehensions.
  • Decorators and context managers make code cleaner and reusable.
  • Asynchronous programming boosts performance.
  • Logging and testing improve maintainability.
  • Virtual environments and packaging help in real-world project deployment.