👑 Premium Sign in Sign up

Language lab · Beginner → AI

🐍 Python

Concept → trace → write code → run tests → explain why → improve → mini-project. Start in the browser if you like; set up your own computer when you are ready.

🧪 Runs in your browserOfficial download & docs ↗

📖 Topics with real code (58)

Every verified output below was produced by running the code with Python 3.12.16 on 6 Nov 2026. Open a topic to edit the code and run it right here in your browser.

🌱 Beginner (13)

Variables and data types

Python decides the type from the value. int is a whole number, float has a decimal point, bool is True or False, str is text. type() shows the type and int(), float(), str() convert between them.

age = 15                 # int
height = 1.62            # float
is_student = True        # bool
name = "Asha"            # str
print(name, age, height, is_student)
print(type(age).__name__, type(height).__name__, type(is_student).__name__, type(name).__name__)
marks = int("88") + 2    # text -> number
print("Marks:", marks, "| as text:", str(marks) + "!")
print("True counts as", int(True), "and False as", int(False))

✅ Verified output

Asha 15 1.62 True
int float bool str
Marks: 90 | as text: 90!
True counts as 1 and False as 0
Operators and boolean logic

Arithmetic (+ - * / // % **), comparison (== != < >) and boolean operators (and, or, not). // is whole-number division and % is the remainder. and/or stop early (short-circuit).

a, b = 17, 5
print(a + b, a - b, a * b, a / b, a // b, a % b, a ** 2)
print(a > b, a == b, a != b)
has_ticket, is_vip = True, False
print("enter:", has_ticket and not is_vip, "| either:", has_ticket or is_vip)
age = 16
print("teen:", 13 <= age <= 19)       # chained comparison

✅ Verified output

22 12 85 3.4 3 2 289
True False True
enter: True | either: True
teen: True
Conditionals: if, elif, else, match

if/elif/else picks one branch. match (Python 3.10+) compares a value against patterns, like switch in other languages.

marks = 78
if marks >= 90:
    grade = "A+"
elif marks >= 75:
    grade = "A"
elif marks >= 35:
    grade = "Pass"
else:
    grade = "Fail"
print("Grade:", grade)

day = "sat"
match day:
    case "sat" | "sun":
        print("Weekend")
    case _:
        print("School day")

✅ Verified output

Grade: A
Weekend
Loops: for, while, break, continue

for walks over a sequence, while repeats while a condition is true. break leaves the loop, continue skips to the next turn. Loops can be nested.

for i in range(1, 6):
    if i == 3:
        continue          # skip 3
    print("for", i)

n = 10
while n > 0:
    if n < 4:
        break             # stop early
    print("while", n)
    n -= 3

for row in range(1, 4):   # nested loop: a small table
    print(" ".join(str(row * col).rjust(2) for col in range(1, 4)))

✅ Verified output

for 1
for 2
for 4
for 5
while 10
while 7
while 4
 1  2  3
 2  4  6
 3  6  9
Functions, parameters and scope

def makes a function. Parameters can have default values and be passed by name. return sends a value back. Variables made inside a function are local to it.

def area(length, width=1):
    return length * width

def greet(name, greeting="Namaste"):
    message = f"{greeting}, {name}!"   # local variable
    return message

print(area(5, 3), area(7))
print(greet("Ravi"), greet(greeting="Hello", name="Meena"))

def stats(*nums):                    # any number of arguments
    return min(nums), max(nums), sum(nums) / len(nums)

low, high, avg = stats(4, 9, 2, 7)
print(low, high, avg)

✅ Verified output

15 7
Namaste, Ravi! Hello, Meena!
2 9 5.5
Integers, floats and rounding

Python integers never overflow: they grow as big as memory allows. Floats are binary, so 0.1 + 0.2 is not exactly 0.3; use round() for display and math.isclose() to compare.

print(2 ** 100)                          # big integers just work
print(0.1 + 0.2, 0.1 + 0.2 == 0.3)
import math
print(math.isclose(0.1 + 0.2, 0.3))
print(round(2.675, 2), round(7.5), round(8.5))   # banker's rounding: halves go to the even number
print(7 // 2, -7 // 2, 7 % 3, -7 % 3)            # floor division rounds down
print(int("101", 2), hex(255), bin(10), f"{1234567.891:,.2f}")

✅ Verified output

1267650600228229401496703205376
0.30000000000000004 False
True
2.67 8 8
3 -4 1 2
5 0xff 0b1010 1,234,567.89
Booleans and truthiness

Every value is true or false in an if. Empty things (0, "", [], {}, None) are false; everything else is true. bool() shows the answer.

for v in [0, 1, -3, "", "hi", [], [0], {}, None, 0.0]:
    print(repr(v).ljust(6), "->", bool(v))
name = ""
print("Hello,", name or "guest")        # or returns the first true value
items = []
if not items:
    print("cart is empty")

✅ Verified output

0      -> False
1      -> True
-3     -> True
''     -> False
'hi'   -> True
[]     -> False
[0]    -> True
{}     -> False
None   -> False
0.0    -> False
Hello, guest
cart is empty
None and default values

None means "no value yet". Test it with is None, not ==. A function without return gives None.

def find(names, target):
    for i, n in enumerate(names):
        if n == target:
            return i
    return None

pos = find(["Asha", "Ravi"], "Meena")
print(pos, pos is None)
result = print("print returns...")
print(result)

✅ Verified output

None True
print returns...
None
Strings: methods and slicing

Strings are sequences of characters. Index from 0 (or -1 from the end), slice with [start:stop:step], and use methods that return new strings.

s = "  GayaGnan Learning  "
t = s.strip()
print(t.upper(), t.lower(), t.title())
print(t[0], t[-1], t[0:8], t[::-1])
print(t.find("Learn"), t.count("a"), t.startswith("Gaya"))
print("-".join(["2025", "06", "01"]), "a,b,,c".split(","))
print("Ravi".center(10, "*"), "7".zfill(3))

✅ Verified output

GAYAGNAN LEARNING gayagnan learning Gayagnan Learning
G g GayaGnan gninraeL nanGayaG
9 4 True
2025-06-01 ['a', 'b', '', 'c']
***Ravi*** 007
while loops and counters

while repeats until its condition becomes false. Keep a counter or a flag so the loop always ends.

n = 27
steps = 0
while n != 1:                 # Collatz sequence
    n = n // 2 if n % 2 == 0 else 3 * n + 1
    steps += 1
print("27 reaches 1 in", steps, "steps")
total, k = 0, 1
while total + k <= 100:
    total += k
    k += 1
print("1 + 2 + ... +", k - 1, "=", total)

✅ Verified output

27 reaches 1 in 111 steps
1 + 2 + ... + 13 = 91
Nested loops and patterns

A loop inside a loop runs the inner one completely for each turn of the outer one. Great for grids, tables and patterns.

for row in range(1, 5):
    print(" " * (4 - row) + "*" * (2 * row - 1))
print()
for i in range(1, 4):
    print("  ".join(f"{i}x{j}={i*j:2}" for j in range(1, 5)))

✅ Verified output

   *
  ***
 *****
*******

1x1= 1  1x2= 2  1x3= 3  1x4= 4
2x1= 2  2x2= 4  2x3= 6  2x4= 8
3x1= 3  3x2= 6  3x3= 9  3x4=12
range, enumerate and zip

range makes number sequences, enumerate gives index and item together, zip walks several lists side by side.

print(list(range(5)), list(range(2, 11, 3)), list(range(5, 0, -1)))
names = ["Asha", "Ravi", "Meena"]
marks = [91, 78, 85]
for i, name in enumerate(names, start=1):
    print(i, name)
for name, m in zip(names, marks):
    print(f"{name:6} {m}")
print(dict(zip(names, marks)))

✅ Verified output

[0, 1, 2, 3, 4] [2, 5, 8] [5, 4, 3, 2, 1]
1 Asha
2 Ravi
3 Meena
Asha   91
Ravi   78
Meena  85
{'Asha': 91, 'Ravi': 78, 'Meena': 85}
Input and parsing numbers

input() always returns text. Split it and convert each part. Here a string stands in for what the user would type.

line = "12 7 30 5"                 # imagine: line = input()
nums = [int(x) for x in line.split()]
print("numbers:", nums)
print("sum:", sum(nums), "max:", max(nums), "min:", min(nums))
print("average: %.2f" % (sum(nums) / len(nums)))
try:
    int("12a")
except ValueError as e:
    print("bad input ->", e)

✅ Verified output

numbers: [12, 7, 30, 5]
sum: 54 max: 30 min: 5
average: 13.50
bad input -> invalid literal for int() with base 10: '12a'

🌿 Intermediate (23)

Lists and tuples

A list is an ordered, changeable collection; a tuple is ordered but fixed. Use indexing, slicing, append, sort and list comprehensions.

marks = [72, 95, 64, 88]
marks.append(91)
marks.sort()
print(marks, "first:", marks[0], "last:", marks[-1], "top 2:", marks[-2:])
squares = [x * x for x in range(1, 6) if x % 2 == 1]
print("odd squares:", squares)
point = (3, 4)                 # tuple
x, y = point                   # unpacking
print("distance:", (x ** 2 + y ** 2) ** 0.5)

✅ Verified output

[64, 72, 88, 91, 95] first: 64 last: 95 top 2: [91, 95]
odd squares: [1, 9, 25]
distance: 5.0
Strings and formatting

Strings are immutable text. Use f-strings for formatting, slicing, split/join and methods like upper(), strip() and replace().

s = "  GayaGnan learning  "
t = s.strip()
print(t.upper(), "|", t.replace("learning", "labs"), "|", len(t))
words = t.split()
print(words, "->", "-".join(words))
price, qty = 49.5, 3
print(f"Total: ₹{price * qty:,.2f} for {qty} items")
print("palindrome:", "level" == "level"[::-1])

✅ Verified output

GAYAGNAN LEARNING | GayaGnan labs | 17
['GayaGnan', 'learning'] -> GayaGnan-learning
Total: ₹148.50 for 3 items
palindrome: True
Dictionaries and sets

A dict maps keys to values; a set keeps unique items. Both find things in about O(1) time.

stock = {"pen": 40, "book": 12}
stock["eraser"] = 25
stock["pen"] -= 5
for item, count in sorted(stock.items()):
    print(item, count)
print("has ruler?", "ruler" in stock, "| book:", stock.get("book", 0))
a, b = {"maths", "science", "art"}, {"science", "music"}
print("both:", a & b, "| either:", sorted(a | b))
words = "the cat and the hat and the bat".split()
freq = {}
for w in words:
    freq[w] = freq.get(w, 0) + 1
print(freq)

✅ Verified output

book 12
eraser 25
pen 35
has ruler? False | book: 12
both: {'science'} | either: ['art', 'maths', 'music', 'science']
{'the': 3, 'cat': 1, 'and': 2, 'hat': 1, 'bat': 1}
Classes and objects

A class bundles data (attributes) and behaviour (methods). __init__ sets up a new object; a leading underscore marks an attribute as internal.

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self._balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("amount must be positive")
        self._balance += amount

    def balance(self):
        return self._balance

acc = BankAccount("Asha", 500)
acc.deposit(250)
print(acc.owner, acc.balance())

✅ Verified output

Asha 750
Inheritance and polymorphism

A subclass reuses a parent class and can override its methods. Code that calls area() works for every shape: that is polymorphism.

import math

class Shape:
    def area(self):
        raise NotImplementedError

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self): return math.pi * self.r ** 2

class Rect(Shape):
    def __init__(self, w, h): self.w, self.h = w, h
    def area(self): return self.w * self.h

for s in [Circle(1), Rect(3, 4)]:
    print(type(s).__name__, round(s.area(), 2), isinstance(s, Shape))

✅ Verified output

Circle 3.14 True
Rect 12 True
Errors and exceptions

try/except catches errors so the program can recover; finally always runs. raise creates your own error.

def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return None
    finally:
        print("checked", a, "/", b)

print(safe_divide(10, 2))
print(safe_divide(1, 0))
try:
    int("abc")
except ValueError as e:
    print("ValueError:", e)

✅ Verified output

checked 10 / 2
5.0
checked 1 / 0
None
ValueError: invalid literal for int() with base 10: 'abc'
Files: write and read

open() with "w" writes and "r" reads. with closes the file automatically. Here we write a small CSV and read it back.

import csv, os, tempfile

path = os.path.join(tempfile.gettempdir(), "marks.csv")
with open(path, "w", newline="") as f:
    w = csv.writer(f)
    w.writerow(["name", "marks"])
    w.writerows([["Asha", 88], ["Ravi", 95]])

with open(path) as f:
    rows = list(csv.DictReader(f))
print(rows)
print("average:", sum(int(r["marks"]) for r in rows) / len(rows))
os.remove(path)

✅ Verified output

[{'name': 'Asha', 'marks': '88'}, {'name': 'Ravi', 'marks': '95'}]
average: 91.5
List comprehensions and filtering

A comprehension builds a new list in one readable line: [expression for item in items if condition]. Also works for dicts and sets.

nums = range(1, 21)
squares = [n * n for n in nums if n % 2 == 0]
print(squares)
words = ["apple", "Banana", "cherry", "date"]
print({w: len(w) for w in words})
print(sorted({w[0].lower() for w in words}))
matrix = [[r * 3 + c for c in range(3)] for r in range(3)]
print(matrix, [row[1] for row in matrix])

✅ Verified output

[4, 16, 36, 64, 100, 144, 196, 256, 324, 400]
{'apple': 5, 'Banana': 6, 'cherry': 6, 'date': 4}
['a', 'b', 'c', 'd']
[[0, 1, 2], [3, 4, 5], [6, 7, 8]] [1, 4, 7]
2D lists: matrices and grids

A list of lists is a grid. grid[r][c] reads one cell. Build it with a comprehension, never [[0]*n]*n, which repeats the same row object.

bad = [[0] * 3] * 3
bad[0][0] = 9
print("shared rows:", bad)
grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 9
print("independent:", grid)
A = [[1, 2], [3, 4]]
B = [[5, 6], [7, 8]]
C = [[sum(A[i][k] * B[k][j] for k in range(2)) for j in range(2)] for i in range(2)]
print("A x B =", C)
print("transpose:", [list(r) for r in zip(*A)])

✅ Verified output

shared rows: [[9, 0, 0], [9, 0, 0], [9, 0, 0]]
independent: [[9, 0, 0], [0, 0, 0], [0, 0, 0]]
A x B = [[19, 22], [43, 50]]
transpose: [[1, 3], [2, 4]]
Tuple unpacking and swapping

Tuples let you return several values and unpack them in one line. * collects the rest.

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

lo, hi = min_max([4, 9, 1, 7])
print(lo, hi)
a, b = 1, 2
a, b = b, a
print(a, b)
first, *middle, last = [10, 20, 30, 40, 50]
print(first, middle, last)
point = (3, 4)
x, y = point
print((x ** 2 + y ** 2) ** 0.5)

✅ Verified output

1 9
2 1
10 [20, 30, 40] 50
5.0
Sets: union, intersection, difference

Sets hold unique items and answer "is x in here?" fast. | & - ^ combine sets.

maths = {"Asha", "Ravi", "Meena", "Kiran"}
science = {"Ravi", "Kiran", "Zara"}
print(sorted(maths | science))
print(sorted(maths & science))
print(sorted(maths - science))
print(sorted(maths ^ science))
print("Zara" in science, len(set("mississippi")))

✅ Verified output

['Asha', 'Kiran', 'Meena', 'Ravi', 'Zara']
['Kiran', 'Ravi']
['Asha', 'Meena']
['Asha', 'Meena', 'Zara']
True 4
Counting with dict, Counter and defaultdict

Counting is the most common dictionary job. Counter does it in one call; defaultdict creates missing keys for you.

from collections import Counter, defaultdict
text = "the cat and the hat and the bat"
counts = {}
for w in text.split():
    counts[w] = counts.get(w, 0) + 1
print(counts)
c = Counter(text.split())
print(c.most_common(2))
groups = defaultdict(list)
for w in text.split():
    groups[len(w)].append(w)
print(dict(groups))

✅ Verified output

{'the': 3, 'cat': 1, 'and': 2, 'hat': 1, 'bat': 1}
[('the', 3), ('and', 2)]
{3: ['the', 'cat', 'and', 'the', 'hat', 'and', 'the', 'bat']}
Sorting with key functions

sorted() returns a new list; list.sort() sorts in place. key= chooses what to compare; reverse=True flips the order. Python's sort is stable.

students = [("Asha", 91, 15), ("Ravi", 78, 16), ("Meena", 91, 14), ("Kiran", 65, 15)]
print(sorted(students, key=lambda s: s[1], reverse=True))
print(sorted(students, key=lambda s: (-s[1], s[0])))     # marks high->low, then name
words = ["banana", "Apple", "cherry"]
print(sorted(words), sorted(words, key=str.lower), sorted(words, key=len))

✅ Verified output

[('Asha', 91, 15), ('Meena', 91, 14), ('Ravi', 78, 16), ('Kiran', 65, 15)]
[('Asha', 91, 15), ('Meena', 91, 14), ('Ravi', 78, 16), ('Kiran', 65, 15)]
['Apple', 'banana', 'cherry'] ['Apple', 'banana', 'cherry'] ['Apple', 'banana', 'cherry']
Stacks and queues with deque

A list is a fine stack (append/pop at the end). For a queue use collections.deque, which removes from the front in O(1).

from collections import deque
stack = []
for page in ["home", "news", "sports"]:
    stack.append(page)
print("back to:", stack.pop(), "->", stack)
q = deque(["Asha", "Ravi"])
q.append("Meena")
print("served:", q.popleft(), "waiting:", list(q))
recent = deque(maxlen=3)
for n in range(1, 7):
    recent.append(n)
print("last 3:", list(recent))

✅ Verified output

back to: sports -> ['home', 'news']
served: Asha waiting: ['Ravi', 'Meena']
last 3: [4, 5, 6]
Priority queue with heapq

heapq keeps the smallest item at index 0. Push and pop are O(log n). Store (priority, item) tuples.

import heapq
tasks = []
heapq.heappush(tasks, (3, "write report"))
heapq.heappush(tasks, (1, "fix server"))
heapq.heappush(tasks, (2, "reply email"))
while tasks:
    pri, job = heapq.heappop(tasks)
    print(pri, job)
print(heapq.nsmallest(3, [9, 4, 7, 1, 8]), heapq.nlargest(2, [9, 4, 7, 1, 8]))

✅ Verified output

1 fix server
2 reply email
3 write report
[1, 4, 7] [9, 8]
Binary search and bisect

Binary search halves a sorted list each step: O(log n). bisect finds insertion points in sorted lists.

def binary_search(xs, target):
    lo, hi, steps = 0, len(xs) - 1, 0
    while lo <= hi:
        steps += 1
        mid = (lo + hi) // 2
        if xs[mid] == target:
            return mid, steps
        if xs[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1, steps

data = list(range(0, 1000, 3))
print(binary_search(data, 501), binary_search(data, 500))
import bisect
grades = [35, 50, 60, 75, 90]
print([("F", "D", "C", "B", "A", "A+")[bisect.bisect(grades, m)] for m in [20, 55, 75, 95]])

✅ Verified output

(167, 8) (-1, 8)
['F', 'C', 'A', 'A+']
Generators and yield

A generator produces values one at a time with yield, so it can describe huge or endless sequences using almost no memory.

def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

from itertools import islice
print(list(islice(fib(), 12)))
squares = (n * n for n in range(1_000_000))     # generator expression: nothing computed yet
print(next(squares), next(squares), sum(n for n in range(101)))

✅ Verified output

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
0 1 5050
Dataclasses and enums

@dataclass writes __init__, __repr__ and == for you. Enum gives names to a fixed set of values.

from dataclasses import dataclass, field
from enum import Enum

class Level(Enum):
    BEGINNER = 1
    ADVANCED = 3

@dataclass(order=True)
class Student:
    marks: int
    name: str
    tags: list = field(default_factory=list)

a = Student(91, "Asha")
b = Student(78, "Ravi", ["captain"])
print(a, a == Student(91, "Asha"), max(a, b).name)
print(Level.ADVANCED, Level.ADVANCED.value, Level(1).name)

✅ Verified output

Student(marks=91, name='Asha', tags=[]) True Asha
Level.ADVANCED 3 BEGINNER
Regular expressions

The re module finds patterns in text: \d digits, \w word characters, + one or more, groups ( ) capture parts.

import re
text = "Call 98765-43210 or 91234-56789. Email: asha@school.in, ravi@college.edu"
print(re.findall(r"\d{5}-\d{5}", text))
for user, domain in re.findall(r"(\w+)@([\w.]+\w)", text):
    print(user, "at", domain)
print(re.sub(r"\d", "#", "PIN 560001"))
print(bool(re.fullmatch(r"[A-Z]{2}\d{2}[A-Z]{2}\d{4}", "KA01AB1234")))

✅ Verified output

['98765-43210', '91234-56789']
asha at school.in
ravi at college.edu
PIN ######
True
JSON: dumps and loads

JSON is the text format APIs use. json.dumps turns Python data into JSON text; json.loads reads it back.

import json
student = {"name": "Asha", "marks": [91, 88], "active": True, "mentor": None}
text = json.dumps(student, sort_keys=True)
print(text)
back = json.loads(text)
print(back["marks"][0] + 1, back["active"], back["mentor"])
print(json.dumps({"a": 1, "b": [1, 2]}, indent=2))

✅ Verified output

{"active": true, "marks": [91, 88], "mentor": null, "name": "Asha"}
92 True None
{
  "a": 1,
  "b": [
    1,
    2
  ]
}
Dates and times

datetime works with dates and clock times; timedelta adds or subtracts days. strftime formats, strptime parses.

from datetime import date, datetime, timedelta
exam = date(2025, 3, 10)
start = date(2025, 1, 1)
print("days to prepare:", (exam - start).days)
print("one week later:", exam + timedelta(weeks=1))
print(exam.strftime("%A, %d %B %Y"))
t = datetime.strptime("2025-03-10 09:30", "%Y-%m-%d %H:%M")
print(t.hour, t.minute, (t + timedelta(hours=3)).strftime("%H:%M"))

✅ Verified output

days to prepare: 68
one week later: 2025-03-17
Monday, 10 March 2025
9 30 12:30
Custom exceptions

Make your own exception class to describe a domain error. Callers can catch exactly that error and the message explains it.

class InsufficientFunds(Exception):
    def __init__(self, need, have):
        super().__init__(f"need {need}, have {have}")
        self.short = need - have

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFunds(amount, balance)
    return balance - amount

print(withdraw(500, 200))
try:
    withdraw(100, 250)
except InsufficientFunds as e:
    print("refused:", e, "| short by", e.short)

✅ Verified output

300
refused: need 250, have 100 | short by 150
Unit tests with assert and unittest

Tests are code that checks code. assert stops on a wrong answer; unittest groups checks and reports each one.

import unittest, io

def is_palindrome(s):
    s = "".join(ch.lower() for ch in s if ch.isalnum())
    return s == s[::-1]

class TestPalindrome(unittest.TestCase):
    def test_simple(self):
        self.assertTrue(is_palindrome("madam"))
    def test_phrase(self):
        self.assertTrue(is_palindrome("Never odd or even"))
    def test_not(self):
        self.assertFalse(is_palindrome("python"))

suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestPalindrome)
result = unittest.TextTestRunner(stream=io.StringIO()).run(suite)
print("ran", result.testsRun, "tests, failures:", len(result.failures), "| ok:", result.wasSuccessful())

✅ Verified output

ran 3 tests, failures: 0 | ok: True

🌳 Advanced (22)

Recursion and memoization

A recursive function calls itself on a smaller problem until a base case. Memoization caches answers so each one is computed only once.

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

def binary_search(items, target, lo=0, hi=None):
    hi = len(items) - 1 if hi is None else hi
    if lo > hi:
        return -1
    mid = (lo + hi) // 2
    if items[mid] == target:
        return mid
    if items[mid] < target:
        return binary_search(items, target, mid + 1, hi)
    return binary_search(items, target, lo, mid - 1)

print([fib(i) for i in range(10)], fib(80))
print(binary_search([2, 5, 8, 12, 16, 23, 38], 23))

✅ Verified output

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 23416728348467685
5
Lambdas, higher-order functions and type hints

Functions are values: pass them to map, filter, sorted and your own functions. Type hints document the expected types.

from typing import Callable, TypeVar
T = TypeVar("T")

def apply_twice(f: Callable[[T], T], x: T) -> T:
    return f(f(x))

students = [("Asha", 88), ("Ravi", 95), ("Meena", 72)]
print(sorted(students, key=lambda s: s[1], reverse=True))
print(list(map(lambda s: s[0].upper(), students)))
print(list(filter(lambda s: s[1] >= 80, students)))
print(apply_twice(lambda n: n * 3, 2), apply_twice(str.upper, "hi"))

✅ Verified output

[('Ravi', 95), ('Asha', 88), ('Meena', 72)]
['ASHA', 'RAVI', 'MEENA']
[('Asha', 88), ('Ravi', 95)]
18 HI
Concurrency: threads and asyncio

Threads help when work waits (network, disk). asyncio runs many waiting tasks on one thread. Both finish here in about 0.2 s, not 0.6 s.

import asyncio, time
from concurrent.futures import ThreadPoolExecutor

def slow_square(n):
    time.sleep(0.2)
    return n * n

start = time.perf_counter()
with ThreadPoolExecutor(max_workers=3) as pool:
    print("threads:", list(pool.map(slow_square, [1, 2, 3])))

async def fetch(i):
    await asyncio.sleep(0.2)
    return f"page{i}"

async def main():
    return await asyncio.gather(*(fetch(i) for i in range(3)))

print("asyncio:", asyncio.run(main()))
print("fast:", time.perf_counter() - start < 0.6)

✅ Verified output

threads: [1, 4, 9]
asyncio: ['page0', 'page1', 'page2']
fast: True
References, mutability and copies

Names point to objects. Changing a mutable object (list, dict) through one name changes it for every name. copy() and deepcopy() make independent copies.

import copy

a = [1, 2, [3, 4]]
b = a                    # same object
c = copy.copy(a)         # new outer list, shared inner list
d = copy.deepcopy(a)     # fully independent
a[2].append(5)
a.append(6)
print("a", a)
print("b", b, b is a)
print("c", c)
print("d", d)

def add_item(item, bucket=None):   # never use a mutable default like bucket=[]
    bucket = [] if bucket is None else bucket
    bucket.append(item)
    return bucket
print(add_item(1), add_item(2))

✅ Verified output

a [1, 2, [3, 4, 5], 6]
b [1, 2, [3, 4, 5], 6] True
c [1, 2, [3, 4, 5]]
d [1, 2, [3, 4]]
[1] [2]
Decorators

A decorator wraps a function to add behaviour (timing, logging, caching, checks) without changing its code. functools.wraps keeps the original name.

import functools

def count_calls(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return fn(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@count_calls
def greet(name):
    return f"Hello, {name}"

print(greet("Asha"), greet("Ravi"))
print(greet.__name__, "called", greet.calls, "times")

✅ Verified output

Hello, Asha Hello, Ravi
greet called 2 times
Context managers and with

with guarantees clean-up (closing files, releasing locks) even when an error happens. Write your own with __enter__/__exit__ or @contextmanager.

from contextlib import contextmanager

@contextmanager
def section(name):
    print(f"[start {name}]")
    try:
        yield name.upper()
    finally:
        print(f"[end {name}]")

with section("backup") as tag:
    print("working on", tag)
try:
    with section("risky"):
        raise RuntimeError("disk full")
except RuntimeError as e:
    print("caught:", e)

✅ Verified output

[start backup]
working on BACKUP
[end backup]
[start risky]
[end risky]
caught: disk full
Closures and nonlocal

An inner function remembers variables from the function that made it (a closure). nonlocal lets it change them.

def make_counter(start=0):
    count = start
    def step(by=1):
        nonlocal count
        count += by
        return count
    return step

c1 = make_counter()
c2 = make_counter(100)
print(c1(), c1(), c1(5), c2(), c1())

✅ Verified output

1 2 7 101 8
Dunder methods and operator overloading

Special methods like __add__, __eq__, __len__ and __repr__ let your objects work with +, ==, len() and print().

class Vector:
    def __init__(self, x, y):
        self.x, self.y = x, y
    def __add__(self, o):
        return Vector(self.x + o.x, self.y + o.y)
    def __mul__(self, k):
        return Vector(self.x * k, self.y * k)
    def __eq__(self, o):
        return (self.x, self.y) == (o.x, o.y)
    def __abs__(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v = Vector(3, 4)
print(v + Vector(1, 1), v * 2, abs(v), v == Vector(3, 4))

✅ Verified output

Vector(4, 5) Vector(6, 8) 5.0 True
Properties and validation

@property makes a method look like an attribute, so you can validate values when they are set without changing how callers use the class.

class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance          # goes through the setter
    @property
    def balance(self):
        return self._balance
    @balance.setter
    def balance(self, value):
        if value < 0:
            raise ValueError("balance cannot be negative")
        self._balance = value

a = Account("Asha", 500)
a.balance += 250
print(a.owner, a.balance)
try:
    a.balance = -10
except ValueError as e:
    print("rejected:", e)

✅ Verified output

Asha 750
rejected: balance cannot be negative
Abstract classes and protocols

An abstract base class forces subclasses to implement methods, so a missing method is caught when an object is created, not later.

from abc import ABC, abstractmethod
import math

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

class Circle(Shape):
    def __init__(self, r): self.r = r
    def area(self): return math.pi * self.r ** 2

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

print([round(s.area(), 2) for s in [Circle(1), Square(3)]])
try:
    Shape()
except TypeError as e:
    print("cannot create:", str(e).split(" with")[0])

✅ Verified output

[3.14, 9]
cannot create: Can't instantiate abstract class Shape
itertools: combinations and products

itertools builds efficient loops: every pair, every ordering, running totals, grouping.

from itertools import combinations, permutations, product, accumulate, groupby
print(list(combinations("ABC", 2)))
print(len(list(permutations(range(4)))), "orderings of 4 items")
print(["".join(p) for p in product("01", repeat=3)])
print(list(accumulate([5, 3, 8, 1])))
print([(key, len(list(grp))) for key, grp in groupby("aaabccdd")])

✅ Verified output

[('A', 'B'), ('A', 'C'), ('B', 'C')]
24 orderings of 4 items
['000', '001', '010', '011', '100', '101', '110', '111']
[5, 8, 16, 17]
[('a', 3), ('b', 1), ('c', 2), ('d', 2)]
functools: lru_cache, partial, reduce

lru_cache remembers results, partial fixes some arguments, reduce folds a list into one value.

from functools import lru_cache, partial, reduce

@lru_cache(maxsize=None)
def ways(n):                     # ways to climb n stairs taking 1 or 2 steps
    return 1 if n <= 1 else ways(n - 1) + ways(n - 2)

print(ways(40), ways.cache_info().hits)
power_of_2 = partial(pow, 2)
print([power_of_2(k) for k in range(6)])
print(reduce(lambda a, b: a * b, range(1, 11)))

✅ Verified output

165580141 38
[1, 2, 4, 8, 16, 32]
3628800
Dynamic programming: knapsack and LCS

Dynamic programming solves a big problem from a table of smaller answers. 0/1 knapsack picks the best set of items; LCS finds the longest common subsequence.

def knapsack(items, cap):
    best = [0] * (cap + 1)
    for w, v in items:
        for c in range(cap, w - 1, -1):
            best[c] = max(best[c], best[c - w] + v)
    return best[cap]

print("knapsack:", knapsack([(1, 1), (3, 4), (4, 5), (5, 7)], 7))

def lcs(a, b):
    T = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
    for i in range(len(a)):
        for j in range(len(b)):
            T[i + 1][j + 1] = T[i][j] + 1 if a[i] == b[j] else max(T[i][j + 1], T[i + 1][j])
    return T[-1][-1]

print("LCS:", lcs("ABCBDAB", "BDCABA"))

✅ Verified output

knapsack: 9
LCS: 4
Graphs: BFS shortest path

Breadth-first search explores a graph level by level from a queue, so the first time it reaches a node is by the fewest edges.

from collections import deque
roads = {"Home": ["School", "Park"], "School": ["Library", "Market"], "Park": ["Market"],
         "Market": ["Station"], "Library": ["Station"], "Station": []}

def bfs(start, goal):
    prev, q = {start: None}, deque([start])
    while q:
        node = q.popleft()
        if node == goal:
            path = []
            while node:
                path.append(node)
                node = prev[node]
            return path[::-1]
        for nxt in roads[node]:
            if nxt not in prev:
                prev[nxt] = node
                q.append(nxt)

print(" -> ".join(bfs("Home", "Station")))

✅ Verified output

Home -> School -> Library -> Station
Merge sort from scratch

Merge sort splits the list in half, sorts each half recursively and merges them: always O(n log n) and stable.

def merge_sort(xs):
    if len(xs) <= 1:
        return xs
    mid = len(xs) // 2
    left, right = merge_sort(xs[:mid]), merge_sort(xs[mid:])
    out, i, j = [], 0, 0
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            out.append(left[i]); i += 1
        else:
            out.append(right[j]); j += 1
    return out + left[i:] + right[j:]

import random
random.seed(7)
data = [random.randint(1, 99) for _ in range(12)]
print(data)
print(merge_sort(data), merge_sort(data) == sorted(data))

✅ Verified output

[42, 20, 51, 84, 7, 10, 69, 13, 47, 75, 8, 65]
[7, 8, 10, 13, 20, 42, 47, 51, 65, 69, 75, 84] True
Bit manipulation

Integers are bits. & | ^ ~ << >> work on them directly: test flags, count set bits, find the single unpaired number.

perms = 0b101                       # read=4, write=2, run=1
print("can read:", bool(perms & 4), "can write:", bool(perms & 2))
perms |= 2
print(bin(perms), bin(perms & ~1))
print([bin(n).count("1") for n in range(8)])
nums = [4, 1, 2, 1, 2]
single = 0
for n in nums:
    single ^= n
print("unpaired:", single, "| 1 << 10 =", 1 << 10, "| is 64 a power of 2:", 64 & 63 == 0)

✅ Verified output

can read: True can write: False
0b111 0b110
[0, 1, 1, 2, 1, 2, 2, 3]
unpaired: 4 | 1 << 10 = 1024 | is 64 a power of 2: True
SQLite database from Python

sqlite3 is built in. Use ? placeholders for values (never string joining) so input cannot inject SQL.

import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE marks (name TEXT, subject TEXT, score INT)")
rows = [("Asha", "Maths", 91), ("Asha", "Science", 85), ("Ravi", "Maths", 78), ("Ravi", "Science", 88)]
db.executemany("INSERT INTO marks VALUES (?, ?, ?)", rows)
for name, avg in db.execute("SELECT name, AVG(score) FROM marks GROUP BY name ORDER BY name"):
    print(name, avg)
evil = "Maths' OR '1'='1"
print(db.execute("SELECT COUNT(*) FROM marks WHERE subject = ?", (evil,)).fetchone()[0], "rows for a hostile input")

✅ Verified output

Asha 88.0
Ravi 83.0
0 rows for a hostile input
Money with Decimal and Fraction

Floats cannot store most decimals exactly. Decimal does exact base-10 money maths with chosen rounding; Fraction keeps exact ratios.

from decimal import Decimal, ROUND_HALF_UP
from fractions import Fraction
print(sum([0.1] * 10), sum([Decimal("0.1")] * 10))
price = Decimal("199.99")
gst = (price * Decimal("0.18")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
print("GST:", gst, "total:", price + gst)
print(Fraction(1, 3) + Fraction(1, 6), Fraction(0.75), Fraction("3/4") * 2)

✅ Verified output

1.0 1.0
GST: 36.00 total: 235.99
1/2 3/4 3/2
Logging instead of print

logging gives levels (DEBUG, INFO, WARNING, ERROR), timestamps and destinations, and can be switched off without editing code.

import logging, sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
log = logging.getLogger("orders")
log.debug("not shown: level is INFO")
log.info("order %s placed", 1042)
log.warning("stock low for %s", "pencils")
try:
    1 / 0
except ZeroDivisionError:
    log.error("could not compute discount")

✅ Verified output

INFO orders: order 1042 placed
WARNING orders: stock low for pencils
ERROR orders: could not compute discount
Command-line arguments with argparse

argparse turns command-line options into variables, with types, defaults and automatic --help. Here we pass a list instead of the real command line.

import argparse
p = argparse.ArgumentParser(prog="report")
p.add_argument("name")
p.add_argument("--marks", type=int, nargs="+", required=True)
p.add_argument("--pass-mark", type=int, default=35)
args = p.parse_args(["Asha", "--marks", "91", "34", "77"])
passed = [m for m in args.marks if m >= args.pass_mark]
print(f"{args.name}: {len(passed)}/{len(args.marks)} passed, best {max(args.marks)}")

✅ Verified output

Asha: 2/3 passed, best 91
Hashing and checksums

A hash turns any data into a fixed-size fingerprint. SHA-256 detects changes; never store passwords as plain hashes, use a slow salted one.

import hashlib
print(hashlib.sha256(b"GayaGnan").hexdigest()[:16])
print(hashlib.sha256(b"Gayagnan").hexdigest()[:16], "(one letter changed)")
salt = b"fixed-demo-salt"                      # real code: os.urandom(16)
key = hashlib.pbkdf2_hmac("sha256", b"my password", salt, 100_000)
print(len(key), "byte key:", key.hex()[:16])

✅ Verified output

8b33418f98922124
b458755cfcb0cd48 (one letter changed)
32 byte key: e9d9964cfea8da22
asyncio.gather with timeouts

async functions pause at await so others can run. gather runs many together; wait_for cancels one that takes too long.

import asyncio

async def fetch(name, delay):
    await asyncio.sleep(delay)
    return f"{name} ok"

async def main():
    results = await asyncio.gather(fetch("marks", 0.05), fetch("timetable", 0.02))
    print(results)
    try:
        await asyncio.wait_for(fetch("slow", 1), timeout=0.05)
    except asyncio.TimeoutError:
        print("slow service timed out")

asyncio.run(main())

✅ Verified output

['marks ok', 'timetable ok']
slow service timed out

🧪 Browser workspace

Runs in a sandbox inside your browser with a time limit: no install needed, and your code never reaches our server.

Loading the workspace…

🛠️ Set up and verify

  1. Install
    winget install --id Python.Python.3.12 -e
    brew install python
    sudo apt install python3 python3-venv python3-pip
  2. Editor: Visual Studio Code with Python (Microsoft), Ruff, Pylance.
  3. Create hello.py
    print('Hello, GayaGnan!')
  4. Run it and check the version
    python hello.py
    python --version   (macOS/Linux: python3 --version)
  5. Health check: paste what your terminal printed.

Install one language at a time. These commands only install or print versions; nothing here deletes files. Versions change: check the official page and note the version you installed.

🧭 The 14-lab path

📚 Modules

  1. print/input, variables and types
  2. conditionals and logic
  3. loops and nested patterns
  4. functions and scope
  5. lists, tuples, sets and dicts
  6. strings and parsing
  7. files, CSV and JSON
  8. exceptions and debugging
  9. modules, packages and venv
  10. OOP and dataclasses
  11. algorithm puzzles
  12. pytest unit tests
  13. HTTP client and mock API
  14. FastAPI CRUD service
  15. async basics
  16. pandas data cleaning and SQL
  17. automation scripts and CLI
  18. agent tool functions (mock tools)

🏗️ Mini-projects

  • Quiz game
  • Expense tracker
  • File organiser simulation
  • CSV quality checker
  • FastAPI student service
  • Safe mock cloud inventory agent

Projects use synthetic data only.