Programming
A practical reference for core programming concepts in Python, JavaScript, and SQL — from built-in types and control flow to higher-order functions, object-oriented patterns, error handling, async programming, and data processing.
Built-in Types
Every language ships with a set of fundamental data types — the building blocks you use before importing any library. Python and JavaScript share many concepts but differ in naming and behaviour.
Overview
The core types that exist natively in Python and JavaScript without any imports.
Numeric Types
Types for representing numbers — integers (whole numbers), floats (decimals), and complex numbers where supported. Python distinguishes int from float explicitly; JavaScript uses a single number type for both.
Think of int as a whole pie and float as a pie slice — one is exact and countable, the other may have imprecise edges due to floating-point arithmetic. Always be aware of which you're working with when doing math.
# Python numeric types
x = 42 # int
y = 3.14 # float
z = 2 + 3j # complex
type(x) # <class 'int'>
isinstance(y, float) # True
int(3.9) # 3 (truncates, not rounds)
Key takeaway: Python's int has arbitrary precision; JavaScript's number is always IEEE 754 double — use BigInt in JS for large integers.
Sequence Types
Ordered collections of items. Python offers list (mutable), tuple (immutable), and range. JavaScript's Array covers both mutable list and tuple roles.
A list is a shopping cart you can keep adding to or rearranging. A tuple is the printed receipt — same order, fixed forever. Use tuples to signal "this data should not change".
# Python sequence types
lst = [1, 2, 3] # list — mutable
tup = (1, 2, 3) # tuple — immutable
rng = range(0, 10, 2) # range — lazy sequence
lst[0] = 99 # OK
# tup[0] = 99 # TypeError!
lst + tup # TypeError — types must match for +
list(rng) # [0, 2, 4, 6, 8]
Key takeaway: Prefer tuples over lists when data is conceptually fixed — it documents intent and can improve performance.
Mapping & Set Types
Key-value stores (dict in Python, plain Object or Map in JS) and unordered unique-value collections (set / Set).
A dict is like a phone book — look up a name (key), get a number (value). A set is like a bag of unique coins — order doesn't matter, duplicates are automatically removed.
# Python mapping and set
d = {'a': 1, 'b': 2}
d['c'] = 3 # add key
d.get('z', 0) # 0 (safe lookup with default)
s = {1, 2, 2, 3} # {1, 2, 3} — duplicates removed
s.add(4)
s & {2, 4, 6} # {2, 4} (intersection)
Key takeaway: Dictionary lookup is O(1) thanks to hash tables — always prefer dict over a list of tuples when you need key-based access.
Python vs JavaScript — Type Names
The same concept often has a different name across languages. This table maps Python built-in types to their nearest JavaScript equivalents.
| Concept | Python | JavaScript | Mutable? |
|---|---|---|---|
| Integer | int |
number / BigInt |
No |
| Decimal | float |
number |
No |
| Text | str |
string |
No |
| Boolean | bool |
boolean |
No |
| Ordered list | list |
Array |
Yes |
| Immutable list | tuple |
Object.freeze([]) |
No |
| Key-value map | dict |
Object / Map |
Yes |
| Unique values | set |
Set |
Yes |
| Nothing / null | None |
null / undefined |
— |
Key takeaway: JavaScript has two "nothing" values (null = explicitly empty, undefined = never set) — Python's single None is simpler and less error-prone.
Flow Control
Flow control determines which code runs, when, and how many times. Mastering conditionals and loops is the foundation of every algorithm.
Condition
Branching execution based on whether an expression is truthy or falsy.
Conditional Statements
The if / elif / else chain (Python) or if / else if / else (JavaScript) tests conditions in order and executes the first matching branch.
Like a sorting machine at a post office: check if the parcel is a letter — if yes, put it here; else if it's a small box — there; else send it to oversize. Only one slot is ever chosen.
# Convert any type to float — Python
if type(x) in [int, float, str, bool]:
y = float(x)
elif type(x) == list: # x = [1, 2, 3]
y = float(x[0]) # y = 1.0
elif type(x) == set: # x = {1, 2, 3}
y = float(x.pop()) # y = 1.0
elif type(x) == dict: # x = {'a': 1, 'b': 2}
y = float(list(x.values())[0])
else:
y = None
Key takeaway: Python uses elif (not else if), relies on indentation instead of braces, and treats many values as falsy (None, 0, [], {}, "").
Loop
Repeating a block of code while a condition holds, with fine-grained control via break and continue.
While Loop
Keeps executing a block as long as its condition is True. Use break to exit early and continue to skip to the next iteration.
Like peeling an onion one layer at a time: keep going while there are layers left. If you hit a rotten spot you want to skip, continue — if you decide to stop entirely, break.
# Extract the first run of digits from a string — Python
x = 'abc123def456'
y = 0
while len(x) > 0:
if (y > 0) and not x[0].isdigit():
break # done collecting digits
elif y == 0 and not x[0].isdigit():
x = x[1:]
continue # skip leading non-digits
y = 10 * y + int(x[0])
x = x[1:]
# y == 123
Key takeaway: Prefer for ... in / for ... of when you know the iteration count; use while when termination depends on a dynamic condition.
For Loop & Iteration Helpers
The for loop iterates over any iterable. Python's enumerate() adds an index counter, zip() pairs two sequences, and range() generates integer sequences — all without materialising intermediate lists. JavaScript's for...of works on any iterable; for...in iterates over object keys.
enumerate is like a museum audio guide that tells you both which exhibit you're at (index) and what it is (value). zip is like pairing each left shoe with its right — it stops as soon as the shorter list runs out.
# for loop, enumerate, zip — Python
fruits = ['apple', 'banana', 'cherry']
prices = [1.2, 0.5, 2.0]
for fruit in fruits:
print(fruit) # apple, banana, cherry
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}") # 1. apple, 2. banana...
for fruit, price in zip(fruits, prices):
print(f"{fruit}: ${price}") # apple: $1.2 ...
for n in range(0, 10, 2): # 0, 2, 4, 6, 8
print(n)
Key takeaway: In JavaScript, prefer for...of for arrays (iterates values) and for...in for objects (iterates keys) — mixing them up is a common source of bugs.
Comprehensions
Concise syntax for building new collections by transforming or filtering existing ones — replacing multi-line loops with a single readable expression.
List, Dict & Set Comprehensions
Python comprehensions produce a new collection in one expression: [expr for item in iterable if condition]. They are faster than equivalent for loops because the loop runs at C speed internally. JavaScript uses .map(), .filter(), and .reduce() for the same purpose.
Instead of: "make an empty list, loop over numbers, check if even, append to list" — just write: "give me all even numbers from this list." Comprehensions read like a sentence and are considered idiomatic Python.
# List comprehension
evens = [x for x in range(10) if x % 2 == 0]
# [0, 2, 4, 6, 8]
squares = [x**2 for x in range(5)]
# [0, 1, 4, 9, 16]
# Dict comprehension
word_lengths = {w: len(w) for w in ['cat', 'elephant', 'ox']}
# {'cat': 3, 'elephant': 8, 'ox': 2}
# Set comprehension (deduplicates automatically)
unique_mods = {x % 3 for x in range(9)}
# {0, 1, 2}
# Generator expression (lazy — no list built in memory)
total = sum(x**2 for x in range(1000000))
Key takeaway: Prefer comprehensions over map() + list() in Python — they're more readable and slightly faster. Use generator expressions (() instead of []) when you only need to iterate once and don't need the list in memory.
Functions
Functions are first-class citizens in Python and JavaScript — they can be passed as arguments, returned from other functions, and decorated to extend behaviour without modifying the original code.
Overview
Higher-order functions, decorators, closures, and recursive patterns.
Higher-Order Functions
A function that takes another function as an argument or returns a function as its result. This enables powerful patterns for building reusable, composable behaviour.
Think of it like a machine that takes a tool as input, applies the tool to some material, and hands back the finished product. The machine doesn't care which tool you pass in — it just knows how to use it.
# func_builder returns an inner function — Python
def mixer(string1, string2):
if min(len(string1), len(string2)) == 0:
return ''
return string1[0] + string2[0] + mixer(string1[1:], string2[1:])
def func_builder(sample, *args, casefunc=str.upper, **kwargs):
def dic_builder(x):
x = casefunc(x)
for arg in args:
x = arg(x)
return {kw: kwargs[kw](x) for kw in kwargs}
return {'function': dic_builder, 'sample_result': dic_builder(sample)}
result = func_builder('hello',
lambda x: mixer(x, '12345'),
lstrip=str.lstrip,
rstrip=str.rstrip)
# result['sample_result'] == {'lstrip': 'H1E2L3L4O5 ',
# 'rstrip': 'H1E2L3L4O5'}
Key takeaway: Functions as values unlocks functional patterns like map, filter, and reduce — always prefer these over manual loops when transforming collections.
Decorators
A decorator wraps a function to extend or modify its behaviour — written with @decorator_name syntax in Python. It is syntactic sugar for func = decorator(func).
Imagine a transparent sleeve over a function — the sleeve adds logging, timing, or caching without touching the original. The function doesn't know it's wrapped, and callers don't need to change either.
import functools
def register_calls(func):
"""Decorator: records every call in a registry dict."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
if args[0].__name__ == 'show_registry':
return wrapper.registry
result = func(*args, **kwargs)
wrapper.registry[len(wrapper.registry)] = (
args[0].__name__, result
)
return result
wrapper.registry = {}
return wrapper
def show_registry(): pass
@register_calls
def call_with_reg(func, x):
return func(x)
call_with_reg(str.upper, 'hello') # 'HELLO' — logged
call_with_reg(show_registry) # {0: ('upper', 'HELLO')}
Key takeaway: Always use @functools.wraps(func) inside a decorator — without it, the wrapper replaces the original function's __name__ and docstring, which breaks debugging.
Closures
A closure is an inner function that remembers variables from its enclosing scope even after the outer function has returned. The inner function "closes over" the variables it references.
Imagine a factory that builds customised stamp-makers. Each stamp-maker remembers the design it was configured with. The factory finishes its job, but each stamp still carries its own memory of its design.
# multiplier_factory returns a closure — Python
def multiplier_factory(factor):
def multiplier(x):
return x * factor # 'factor' lives in the closure
return multiplier
double = multiplier_factory(2)
triple = multiplier_factory(3)
double(5) # 10
triple(5) # 15
# 'factor' is gone from scope but both closures
# each hold their own copy of it
Key takeaway: Closures are the foundation of Python's decorator pattern and JavaScript's module pattern — any time state needs to outlive a function call without using a class, reach for a closure.
Lambda & Arrow Functions
Anonymous functions defined inline — useful for short, one-off operations passed to higher-order functions.
Lambda & Arrow Functions
Python's lambda creates a nameless function limited to a single expression. JavaScript's arrow functions (=>) are more powerful: they can have a block body, and crucially they do not rebind this — making them the standard choice for callbacks inside class methods.
A lambda is a disposable tool. Instead of naming a function just to pass it once, you write it inline: sorted(people, key=lambda p: p.age). In JS, arrow functions also solve the infamous this problem — they borrow this from the surrounding scope rather than creating their own.
# lambda: anonymous single-expression function
square = lambda x: x ** 2
square(5) # 25
# Most common use: as a key function
people = [('Alice', 30), ('Bob', 25), ('Carol', 35)]
sorted(people, key=lambda p: p[1])
# [('Bob', 25), ('Alice', 30), ('Carol', 35)]
# With map and filter
list(map(lambda x: x * 2, [1, 2, 3])) # [2, 4, 6]
list(filter(lambda x: x > 1, [0, 1, 2, 3])) # [2, 3]
# Lambda is limited to ONE expression — use def for multi-line
Key takeaway: Prefer list comprehensions over map(lambda ...) in Python — comprehensions are more readable. Use lambda when passing a key function to sorted(), min(), or max().
Recursion & Memoization
Functions that call themselves to solve a problem by reducing it to smaller instances — and caching to avoid redundant computation.
Recursion & @lru_cache
A recursive function calls itself with a simpler subproblem until it reaches a base case. Without memoization, naive recursion recomputes the same subproblems exponentially. Python's @functools.lru_cache stores previous results automatically. JavaScript can use a Map as a manual cache or a generic memoize wrapper.
Fibonacci without caching recalculates fib(30) over a billion times. With @lru_cache, each unique input is calculated exactly once and stored — turning an O(2ⁿ) algorithm into O(n). It's like writing your answers on a notepad instead of solving the same math problem from scratch every time.
from functools import lru_cache
# Without cache: O(2^n) — extremely slow for large n
def fib_slow(n):
if n <= 1: return n
return fib_slow(n - 1) + fib_slow(n - 2)
# With cache: O(n) — each value computed once
@lru_cache(maxsize=None)
def fib(n):
if n <= 1: return n
return fib(n - 1) + fib(n - 2)
fib(50) # 12586269025 — instant
fib.cache_info() # CacheInfo(hits=48, misses=51, ...)
# Recursion for tree traversal
def flatten(lst):
result = []
for item in lst:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result
flatten([1, [2, [3, 4]], 5]) # [1, 2, 3, 4, 5]
Key takeaway: Python's default recursion limit is 1 000 frames — for deep recursion, use iterative solutions with an explicit stack or increase the limit with sys.setrecursionlimit().
Objects
Object-oriented programming lets you model the world as interacting entities with state and behaviour. Python leans on metaclasses and multiple inheritance; JavaScript uses prototype chains and Proxies.
Classes & Inheritance
Python metaclasses intercept class creation itself — letting you instrument or transform classes as objects.
Metaclasses
A metaclass is the class of a class. When Python creates a new class, it calls the metaclass's __new__ and __init__. This lets you hook into and transform class creation — adding methods, enforcing constraints, or logging instantiation.
A class creates objects. A metaclass creates classes. It is the blueprint of blueprints — if you want to add a "last modified" timestamp to every class in your framework automatically, that is a job for a metaclass.
class Meta(type):
def __new__(meta, name, bases, dct):
# called when a new class is *defined*
Recorder.record += name + '(n)'
return super(Meta, meta).__new__(meta, name, bases, dct)
def __init__(cls, name, bases, dct):
# called when a new class is *initialised*
Recorder.record += name + '(i)'
super(Meta, cls).__init__(name, bases, dct)
def __call__(cls, *args, **kwargs):
# called when the class is *instantiated*
Recorder.record += cls.__name__ + '(c)'
return type.__call__(cls, *args, **kwargs)
# Dynamically create a class using Meta
P1 = Meta('P1', (), {'__init__': lambda self: None})
# Recorder.record == 'P1(n)P1(i)'
P1()
# Recorder.record == 'P1(n)P1(i)P1(c)'
Key takeaway: Metaclasses are powerful but rarely needed in application code. Reach for them in frameworks and ORMs — where you need to enforce class-level contracts across many user-defined subclasses.
Multiple Inheritance & MRO
Python supports multiple inheritance — a class can inherit from several parents. Python's Method Resolution Order (MRO) uses C3 linearisation to determine which parent's method wins when there is a conflict.
If a child class has two parents and both define greet(), which one wins? Python walks a strict left-to-right depth-first order (the MRO) and picks the first match. Call ClassName.__mro__ to inspect the order.
class A:
def greet(self): return 'A'
class B(A):
def greet(self): return 'B'
class C(A):
pass # inherits A.greet
class D(B, C):
pass
D().greet() # 'B' — MRO: D → B → C → A
D.__mro__ # (<D>, <B>, <C>, <A>, <object>)
super(B, D()).greet() # 'A' — skips B in the chain
Key takeaway: Always call super().__init__() in multi-inheritance hierarchies — it ensures all parent __init__ methods in the MRO chain are called exactly once.
Dunder / Magic Methods
Special double-underscore methods that let user-defined classes hook into Python's built-in operations like printing, comparison, arithmetic, and container access.
Dunder Methods (__repr__, __len__, __add__)
Dunder (double-underscore) methods define how instances behave with Python's built-in functions and operators. __repr__ / __str__ control string display, __len__ enables len(), __add__ overloads +, and __getitem__ / __setitem__ make objects subscriptable with [].
When you write len(my_obj), Python calls my_obj.__len__(). Dunders are the hooks that let your class play nicely with Python's built-in syntax. Once you implement them, your objects feel native — they work with sorted(), in checks, +, and everything else you'd expect.
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self): # repr(v) → "Vector(1, 2)"
return f"Vector({self.x}, {self.y})"
def __add__(self, other): # v1 + v2
return Vector(self.x + other.x, self.y + other.y)
def __len__(self): # len(v)
return 2
def __eq__(self, other): # v1 == v2
return self.x == other.x and self.y == other.y
def __getitem__(self, idx): # v[0], v[1]
return (self.x, self.y)[idx]
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v1 + v2 # Vector(4, 6)
len(v1) # 2
v1[0] # 1
v1 == Vector(1, 2) # True
Key takeaway: Always implement __repr__ first — it's used in the REPL, debugging, and logging. If you implement __eq__, also implement __hash__ (or set it to None) — otherwise your objects won't work in sets or as dict keys.
Iterables
Generators produce values one at a time on demand — they are memory-efficient iterables that can pause and resume execution via yield.
Generators & yield
A generator function uses yield instead of return. Each call to next() resumes the function from where it paused. You can also send values back in via generator.send(value).
A generator is like a vending machine that dispenses one item at a time. It waits patiently between each dispense. Unlike a list that loads everything into memory, a generator produces each item only when asked — ideal for large or infinite sequences.
import string, random, statistics
def random_character_generator():
chars_to_remove = ""
while True:
chars = string.ascii_lowercase
for ch in chars_to_remove:
chars = chars.replace(ch, "")
char_to_yield = random.choice(chars)
chars_to_remove = (yield char_to_yield)
# caller sends back letters to exclude
rcg = random_character_generator()
rcg.send(None) # prime the generator
result = []
for _ in range(20):
c = rcg.send('aeiou') # exclude vowels
result.append(c)
# result contains 20 consonants
Key takeaway: Use generators whenever you process a sequence larger than fits in memory, or when you need a lazy pipeline — yield from lets generators delegate to sub-generators cleanly.
Custom Iterables (__iter__ / __next__)
Any class that implements __iter__ (returning self) and __next__ (returning the next value or raising StopIteration) can be used in a for loop.
The iterator protocol is like a playlist interface — as long as your object knows "what's current" and "what's next", Python's for loop and list comprehensions will work with it natively, no changes needed on their side.
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
self.current -= 1
return self.current + 1
list(Countdown(5)) # [5, 4, 3, 2, 1]
for n in Countdown(3):
print(n) # 3, 2, 1
Key takeaway: A generator function is the easiest way to make an iterable — only implement the full __iter__ / __next__ protocol when you need the class to carry additional state or methods alongside iteration.
Data Processing
Python's ecosystem excels at tabular data. Pandas is the go-to library for DataFrames — think of it as Excel inside Python. NumPy underpins Pandas and is the standard for numerical array operations. SQL remains the lingua franca for querying relational databases and mirrors many Pandas operations.
Selection & Reshaping
Selecting subsets of rows and columns, and reshaping data between wide and long formats (pivot / melt / stack).
DataFrame Selection with MultiIndex
Pandas .loc selects by label; .iloc by integer position. pd.IndexSlice makes multi-level index selection readable. NumPy arrays support similar slicing but operate on homogeneous numerical data.
Selecting from a MultiIndex DataFrame is like addressing a grid with named row groups and column groups — instead of saying "row 3, column 2" you say "rows where N='B', column ('c',4)". More readable, less error-prone.
import pandas as pd
df = pd.DataFrame(
[[11, 12, 13], [14, 15, 16],
[17, 18, 19], [20, 21, 22]],
index=pd.MultiIndex.from_product(
[['A', 'B'], [1, 2]], names=['N', 'V']),
columns=pd.MultiIndex.from_tuples(
[('c', 3), ('c', 4), ('d', 4)], names=['n', 'v']))
idx = pd.IndexSlice
# Select rows where N='B', column ('c', 4)
df.loc[idx['B', :], [('c', 4)]]
# (c, 4)
# (B, 1) 18
# (B, 2) 21
Key takeaway: Prefer .loc over .iloc in production code — label-based selection is robust to row reordering; integer-position selection silently breaks when the DataFrame changes shape.
Pivot & Melt (Wide ↔ Long)
pd.melt() unpivots a wide DataFrame to long format (one row per observation). .pivot_table() does the reverse — turning long format back to wide. The SQL equivalents are UNPIVOT and conditional aggregation.
Wide format: one row per person, each year's score in its own column. Long format: one row per (person, year) pair with a single "score" column. Long format is easier to plot and aggregate; wide is easier to read.
# Wide → Long → Wide round-trip — Python (Pandas)
dg = pd.melt(df.reset_index(), id_vars=['N', 'V'])
# each column becomes a separate row
dg = dg.pivot_table(
index=['N', 'V'],
columns=['n', 'v'],
values=['value'])
dg.columns = dg.columns.droplevel(0) # drop extra level
all(df == dg) # True — round-trip is lossless
Key takeaway: Store data in long format in databases — it is more normalised, easier to extend with new variables, and naturally handled by GROUP BY. Pivot to wide only for display or for specific algorithms that expect it.
Merging
Combining DataFrames by rows (concat) or by matching keys (merge) — directly equivalent to SQL UNION and JOIN.
Concat & Merge (Join Types)
pd.concat() stacks DataFrames along rows or columns. pd.merge() performs SQL-style joins (inner, left, right, outer) on a key column. The indicator=True flag adds a _merge column showing where each row came from.
Concat is like physically stacking two spreadsheets. Merge is like a Venn diagram — inner keeps only the overlap, outer keeps everything (filling gaps with NaN), left keeps the left sheet's rows even without a match.
import pandas as pd
df = pd.DataFrame({'a': [1, 2], 'b': [3, 4]}, index=['A', 'B'])
dg = pd.DataFrame({'b': [4, 5], 'c': [6, 7]}, index=['C', 'A'])
# Row-stack (like SQL UNION ALL)
pd.concat([df, dg])
# a b c
# A 1.0 3.0 NaN
# B 2.0 4.0 NaN
# C NaN 4.0 6.0
# A NaN 5.0 7.0
# Outer join on column 'b' (like SQL FULL OUTER JOIN)
pd.merge(df, dg, on='b', how='outer', indicator=True)
# a b c _merge
# 0 1.0 3 NaN left_only
# 1 2.0 4 6.0 both
# 2 NaN 5 7.0 right_only
Key takeaway: Use indicator=True when debugging merges — the _merge column immediately reveals unexpected duplicates or missing matches that would otherwise silently corrupt downstream analysis.
SQL Join Types
SQL's join vocabulary maps directly to Pandas merge modes. Understanding the four core join types is essential for writing correct queries.
Think of two circles in a Venn diagram. INNER JOIN returns only the overlap. LEFT JOIN returns everything from the left circle plus the overlap. RIGHT JOIN is the mirror. FULL OUTER JOIN returns both circles entirely — filling unmatched sides with NULL.
-- SQL joins — equivalent to Pandas merge how= modes
-- INNER JOIN (how='inner'): only matched rows
SELECT df.a, df.b, dg.c
FROM df INNER JOIN dg ON df.b = dg.b;
-- LEFT JOIN (how='left'): all df rows + matches from dg
SELECT df.a, df.b, dg.c
FROM df LEFT JOIN dg ON df.b = dg.b;
-- FULL OUTER via LEFT + RIGHT UNION (MySQL workaround)
SELECT df.a, df.b, dg.c
FROM df LEFT JOIN dg ON df.b = dg.b
UNION ALL
SELECT df.a, dg.b, dg.c
FROM df RIGHT JOIN dg ON df.b = dg.b
WHERE df.b IS NULL;
-- UNION (stack rows, deduplicate — like pd.concat + drop_duplicates)
SELECT * FROM df
UNION
SELECT * FROM dg;
Key takeaway: UNION deduplicates rows; UNION ALL keeps duplicates and is faster. Use UNION ALL unless you explicitly need deduplication.
GroupBy & Aggregation
Splitting data into groups, applying a function to each group, and combining results — the core pattern behind summary statistics, reports, and dashboards.
GroupBy, Aggregation & Window Functions
Pandas groupby() splits a DataFrame by unique values in one or more columns, then .agg() applies one or more functions per group. transform() returns a result with the same shape as the original — useful for adding a group statistic back as a column. SQL's GROUP BY + window functions (OVER PARTITION BY) are the exact equivalents.
Imagine a spreadsheet of sales — one row per transaction. GroupBy is "calculate totals per region." transform is "add each transaction's regional total as a new column so I can compute each transaction's % share." The difference: agg shrinks the table; transform keeps it the same size.
import pandas as pd
sales = pd.DataFrame({
'region': ['N','N','S','S','S'],
'product': ['A','B','A','B','A'],
'revenue': [100,200,150,80,120]
})
# agg — shrinks to one row per group
sales.groupby('region')['revenue'].agg(['sum', 'mean', 'count'])
# sum mean count
# N 300 150.0 2
# S 350 116.7 3
# Multiple grouping keys + custom aggregation
sales.groupby(['region', 'product']).agg(
total=('revenue', 'sum'),
n=('revenue', 'count')
)
# transform — keeps original shape, adds group stat
sales['region_total'] = sales.groupby('region')['revenue'].transform('sum')
sales['pct_of_region'] = sales['revenue'] / sales['region_total'] * 100
Key takeaway: Use transform() instead of agg() + merge() when you need to add a group-level statistic back onto the original rows — it's cleaner and avoids a join.
Error Handling
Errors are inevitable — the question is whether your program crashes or recovers gracefully. Both Python and JavaScript use a try/catch pattern, but Python's is more expressive with multiple except clauses, else, and finally.
try / except / finally
Catching and recovering from exceptions — using finally for cleanup that must always run.
try / except / else / finally
Python's try block runs code that might fail. except catches specific exception types, else runs only if no exception occurred, and finally always runs — perfect for releasing resources (files, database connections). JavaScript uses try / catch / finally with the same structure.
Think of try/except like crossing a rope bridge: try is the attempt, except catches you if you fall, else celebrates if you made it safely, and finally retracts the bridge regardless of what happened. Always be specific about which exception you catch — catching Exception blindly hides bugs.
# Python: try / except / else / finally
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Cannot divide by zero")
return None
except TypeError as e:
print(f"Type error: {e}")
return None
else:
print("Division succeeded") # only if no exception
return result
finally:
print("Cleanup — always runs")
# File handling with context manager (preferred over try/finally)
with open('data.txt', 'r') as f:
content = f.read()
# file is automatically closed even if read() raises
Key takeaway: Python's with statement (context manager) is cleaner than manual try/finally for resource cleanup — implement __enter__ / __exit__ on your own classes to support it.
Custom Exceptions
Defining your own exception hierarchy so callers can catch domain-specific errors without relying on generic built-in types.
Custom Exception Classes
Inheriting from Exception (Python) or Error (JavaScript) lets you create named, catchable exceptions that carry domain-specific context. A well-designed exception hierarchy lets callers catch at exactly the right level of specificity — except ValidationError is far clearer than except ValueError.
Built-in exceptions are like generic medical codes. Custom exceptions are like specific diagnoses — "PaymentDeclinedError" tells you exactly what went wrong and lets you handle it differently from "NetworkTimeoutError" even though both could be caught as "AppError" at the top level.
# Custom exception hierarchy — Python
class AppError(Exception):
"""Base class for all application errors."""
class ValidationError(AppError):
def __init__(self, field, message):
self.field = field
super().__init__(f"{field}: {message}")
class NotFoundError(AppError):
def __init__(self, resource, id_):
super().__init__(f"{resource} {id_} not found")
# Raise and catch at the right level
def get_user(user_id):
if not isinstance(user_id, int):
raise ValidationError('user_id', 'must be an integer')
if user_id == 0:
raise NotFoundError('User', user_id)
try:
get_user(0)
except ValidationError as e:
print(f"Invalid input: {e.field}")
except NotFoundError:
print("Resource missing")
except AppError:
print("General app error") # catches all subclasses
Key takeaway: Always re-raise unexpected exceptions — catching except Exception (Python) or a bare catch (JS) without re-throwing hides bugs. In JavaScript, always set this.name = this.constructor.name in custom errors so stack traces are readable.
Async Programming
Asynchronous code lets a program do other work while waiting for slow operations — network calls, disk I/O, timers. Both Python and JavaScript use async / await, but their underlying models differ: Python uses an explicit event loop (asyncio), while JavaScript's event loop is always running in the runtime.
async / await
Writing asynchronous code that reads like synchronous code — pausing at await points without blocking the thread.
async / await & Concurrent Tasks
An async function always returns a coroutine (Python) or Promise (JavaScript). await suspends the current coroutine until the awaited result is ready, yielding control back to the event loop so other tasks can run. Running tasks concurrently with asyncio.gather() (Python) or Promise.all() (JS) is far faster than awaiting them one at a time.
Awaiting tasks in sequence is like ordering coffee, waiting until it arrives, then ordering toast, then waiting. Using gather() / Promise.all() is like ordering both at the same time and picking up whichever arrives first. The total wait time is the longest individual wait, not the sum of all waits.
import asyncio
async def fetch_data(url: str) -> dict:
# Simulate a network call
await asyncio.sleep(1)
return {'url': url, 'data': '...'}
async def main():
# Sequential — takes 3 seconds total
r1 = await fetch_data('/a')
r2 = await fetch_data('/b')
r3 = await fetch_data('/c')
# Concurrent — takes ~1 second total
results = await asyncio.gather(
fetch_data('/a'),
fetch_data('/b'),
fetch_data('/c'),
)
return results
asyncio.run(main())
Key takeaway: The most common async mistake is putting await inside a loop — each iteration waits before starting the next. Instead, create all tasks first, then await asyncio.gather() / Promise.all() to run them concurrently.
Promises & Event Loop
How JavaScript's single-threaded event loop handles concurrency — and how Promises represent eventual values.
Promises & the Event Loop
A Promise is an object representing the eventual result of an asynchronous operation — it is either pending, fulfilled, or rejected. JavaScript's event loop processes the call stack synchronously, then drains the microtask queue (Promise callbacks) before handling the next macrotask (setTimeout, I/O). Python's asyncio uses an explicit event loop that must be started with asyncio.run().
The event loop is a waiter who handles one table at a time. When a table (task) says "I'm waiting for my food (I/O)", the waiter moves on to another table. When the kitchen (OS) signals "food's ready", the waiter adds that table back to the queue. No one is ever blocked — the waiter is always busy.
// Promises — explicit .then() chain (pre-await style)
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 1000);
});
promise
.then(result => { console.log(result); return result + '!'; })
.then(result => console.log(result)) // 'done!'
.catch(err => console.error(err))
.finally(() => console.log('settled'));
// Promise combinators
Promise.all([p1, p2, p3]) // all must succeed
Promise.allSettled([p1, p2, p3]) // waits for all, ignores failures
Promise.race([p1, p2, p3]) // first to settle wins
Promise.any([p1, p2, p3]) // first to SUCCEED wins
// Event loop ordering demo
console.log('1 sync');
setTimeout(() => console.log('3 macro'), 0);
Promise.resolve().then(() => console.log('2 micro'));
// Output: 1 sync → 2 micro → 3 macro
Key takeaway: In JavaScript, microtasks (Promise callbacks) always run before the next macrotask (setTimeout). This means code immediately after Promise.resolve().then() runs before a setTimeout(..., 0) — a subtle ordering effect that trips up many developers.