Exception Handling — try / except / else / finally
What: Exception handling is the safety net around risky code — try the operation, except catches known failures (ZeroDivision, ValueError, FileNotFound), else runs only if nothing failed, and finally always runs (close handles, release connections). Why it matters: In ETL every stage is a minefield: a zero divisor in a rate calc, a bad string in an int column, a missing landing file — wrapping them with specific except guards keeps the pipeline alive and logs a clean error instead of crashing at 3 AM. How to remember: raise Exception('insufficient balance') is you throwing the error; except (ValueError, ZeroDivisionError) as e is you catching it — be specific, never bare except:.
x = 10/0
ZeroDivision / ValueError
success path
file / connection
except FileNotFoundError: | Custom gate → if amount > balance: raise Exception('insufficient')try:
x = 10/0
except ZeroDivisionError:
print('you cannot divide by 0')
else:
print('no error')
finally:
print('i will execute no matter what')
try:
y = int('abc')
except (ValueError, ZeroDivisionError) as e:
print('Error:', e)
def withdraw(amount):
balance = 1000
if amount > balance:
raise Exception('insufficient balance')
return balance - amount
try:
print(withdraw(10000))
except Exception as e:
print('Error:', e)
try:
file = open('/tmp/sample.csv','r')
except FileNotFoundError:
print('file not found')
else:
print('file read successfully')
finally:
print('im just printing')you cannot divide by 0
i will execute no matter what
Error: invalid literal for int() with base 10: 'abc'
Error: insufficient balance
file not found
im just printingFile Handling — r / w / a
What: File handling in Python is open(path, mode) where mode decides the contract — 'w' creates/overwrites, 'r' reads, 'a' appends — and with open(...) as f: auto-closes the handle even if an error occurs. Why it matters: Before Spark's spark.read.csv touches a 5 GB log, Python often pre-checks or samples it with open; using /tmp/t3.txt keeps examples portable across local and cluster nodes, and a generator that yields lines streams the file without loading it all into RAM. How to remember: w = write (fresh slate), a = add (keep old), r = read (look only) — and always prefer with over manual close().
f.write('welcome')
for line in f: ...
with open(...) as f: auto-close | Large file: def readlog(path): with open(path) as fh: for line in fh: yield line streams without OOMspark.read.csv('/data/...') at scale
Python: pre-check sample before Spark load
with open('/tmp/t3.txt','w') as f:
f.write('hello world ')
f.write('welcome to python')
with open('/tmp/t3.txt','r') as f:
print(f.read()) # hello world welcome to python
with open('/tmp/t3.txt','a') as f:
f.write('\nthis is a new line appended')
with open('/tmp/t3.txt','r') as f:
for line in f: print(line.strip())
import os; os.remove('/tmp/t3.txt'); print('removed')
# Generator-style line reader — 5GB log without OOM
def readlogfile(path):
with open(path,'r') as fh:
for line in fh:
yield linehello world welcome to python
hello world welcome to python
this is a new line appended
removedRegex — phones, emails, dates, names
What: Regular expressions are pattern languages for text — \b\d{10}\b means "exactly 10 digits as a whole word", \b\d{2}[-/]\d{2}[-/]\d{4}\b matches dates like 16-05-1994, and [A-Z][a-z]+ finds Capitalized Words. Why it matters: In ETL you extract PII from free-text logs: phones, emails, postcodes, and HTML snippets — one re.findall(pattern, txt) replaces 20 lines of manual string slicing, and {2,} quantifiers make the rules resilient. How to remember: \b = word edge, \d = digit, {n} = exactly n, {2,} = two or more — compose them left to right like LEGO.
{10} exactly 10 {6} postcode {2,} TLD ≥2 chars | Use re.findall(pattern, txt) to extract all matchesimport re
txt = 'Praveen Kumar bought a Tomato Ketchup and he was born on 16-05-1994. His email is praveen.b@example.com and Phone Is 6363402404 and 1231231231.'
print(re.findall(r'\b[A-Z][a-z]+\s[A-Z][a-z]+\b', txt)) # ['Praveen Kumar','Tomato Ketchup']
print(re.findall(r'\b\d{10}\b', txt)) # ['6363402404','1231231231']
print(re.findall(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b', txt)) # ['praveen.b@example.com']
print(re.findall(r'\b\d{2}[-/]\d{2}[-/]\d{4}\b', txt)) # ['16-05-1994']
print(re.findall(r'\b\d{6}\b', '560001 is Bangalore')) # ['560001']
print(re.findall(r'<p>(.*?)</p>', '<p>hello world</p>')) # ['hello world']['Praveen Kumar', 'Tomato Ketchup']
['6363402404', '1231231231']
['praveen.b@example.com']
['16-05-1994']
['560001']
['hello world']JSON & APIs — dumps/loads & HTTP codes
What: JSON is the lingua franca of APIs and streaming — json.loads(string) parses text into a Python dict/list (load string), while json.dumps(object) serialises a dict back to a JSON string (dump string). Why it matters: Every REST response, Kinesis payload, and Spark spark.read.json(...) row is JSON; mastering the round-trip plus HTTP codes (2xx success, 4xx client error, 5xx server error) and verbs (GET read, POST create, PUT/PATCH/DELETE) is table stakes for ingesting external data. How to remember: Loads goes Left (string → object), Dumps goes Direction outward (object → string) — and Python True/None become JSON true/null.
"age":30}'
'age':30}
import json
s = '{"name":"praveen","age":30,"city":"bangalore"}'
obj = json.loads(s); print(obj, type(obj))
obj2 = {'name':'praveen','age':30,'city':'bangalore'}
s2 = json.dumps(obj2); print(s2, type(s2))
x = {'name':'John','married':True,'children':('Ann','Billy'),'pets':None,
'cars':[{'model':'BMW 230','mpg':27.5},{'model':'Ford Edge','mpg':24.1}]}
print(json.dumps(x))
# Status: 1xx info, 2xx success, 3xx redirect, 4xx client, 5xx server
# GET=read, POST=create, PUT=full update, PATCH=partial, DELETE=remove{'name': 'praveen', 'age': 30, 'city': 'bangalore'} <class 'dict'>
{"name": "praveen", "age": 30, "city": "bangalore"} <class 'str'>
{"name": "John", "married": true, ...}Decorators & Generators
What: A decorator wraps a function with extra behaviour — @loginrequired checks login before viewprofile() runs — and a generator uses yield to produce values one-by-one instead of building a huge list. Why it matters: Decorators add cross-cutting concerns (auth, timing, retry) without touching core logic, while generators stream a 5 GB log line-by-line with constant memory — both are essential for production Spark helpers that must scale without OOM. How to remember: Decorator = gift wrapper around the function; Generator = faucet that drips one line at a time when you call next().
wrapper() runs before/after func()def countup(n): yield ifor line in fh: yield line — never loads alldef loginrequired(func):
def wrapper(user):
if user['loggedin']: return func(user)
else: print('please login before accessing profile')
return wrapper
@loginrequired
def viewprofile(user): print('welcome how are you ?')
viewprofile({'name':'praveen','loggedin':True}) # welcome
viewprofile({'name':'praveen','loggedin':False}) # please login...
def mydecorator(func):
def wrapper():
print('before')
func()
print('after')
return wrapper
@mydecorator
def idli(): print('idli is hot')
idli() # before / idli is hot / after
def readfile(path):
with open(path,'r') as f:
for line in f: yield line
def countup(n):
for i in range(n): yield i
print(list(countup(3))) # [0,1,2]welcome how are you ?
please login before accessing profile
before
idli is hot
after
[0, 1, 2]