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.
📖 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
- Install
winget install --id Python.Python.3.12 -e
brew install python
sudo apt install python3 python3-venv python3-pip
- Editor: Visual Studio Code with Python (Microsoft), Ruff, Pylance.
- Create
hello.pyprint('Hello, GayaGnan!') - Run it and check the version
python hello.py python --version (macOS/Linux: python3 --version)
- 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
- 00 · Setup and verify — install the runtime and editor, run hello world, print the version, fix PATH
- 01 · Syntax and trace — follow each line and the value of every variable
- 02 · Logic — conditionals and decision trees 🧪 Tic-Tac-Toe Referee
- 03 · Loops — trace tables and loop challenges 🧪 Loop Carnival
- 04 · Functions — break problems down and test each piece 🧪 Function Factory
- 05 · Data structures — lists, maps, stacks, queues and trees you can see 🧪 Linked List Builder
- 06 · Debugging — bug cases with a hint ladder 🧪 Debugging Detective
- 07 · Algorithms — search, sort and Big-O comparison 🧪 Number Guessing Strategy
- 08 · Files and data — read and write fixture files
- 09 · API or database — talk to a mock API or a sandbox database 🧪 Tiny Weather Mock API
- 10 · Testing — write tests and read failing assertions 🧪 Unit Test Challenge
- 11 · Security — secure coding and dependency hygiene for this language 🧪 Secure Coding: Validation & Injection
- 12 · Project — one portfolio mini-project 🧪 Board Game Simulator
- 13 · Assessment — quiz, coding test, rubric and an explain-your-answer prompt
📚 Modules
- print/input, variables and types
- conditionals and logic
- loops and nested patterns
- functions and scope
- lists, tuples, sets and dicts
- strings and parsing
- files, CSV and JSON
- exceptions and debugging
- modules, packages and venv
- OOP and dataclasses
- algorithm puzzles
- pytest unit tests
- HTTP client and mock API
- FastAPI CRUD service
- async basics
- pandas data cleaning and SQL
- automation scripts and CLI
- 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.