Python Programming - Placement Notes
Python Programming — Complete Study Notes
1. Introduction to Python
2. Variables, Input/Output
3. Strings and String Methods
4. Numbers
5. Functions
6. Conditionals and Boolean Logic
7. Loops and Data Structures
8. Exception Handling
9. Libraries and Modules
10. File I/O
11. Object-Oriented Programming (missing entirely — essential for placements)
12. Generators and Iterators (missing — commonly asked)
13. Decorators (missing — common interview topic)
14. Unit Testing
15. Time & Space Complexity Basics (missing — asked in almost every placement
interview)
16. Placement Interview — Quick Q&A Revision
17. Key Design Principles
Python Programming — Complete Study Notes
(Based on CS50 Python + Placement-Ready Additions)
These notes cover core Python from the ground up, plus the extra topics interviewers commonly expect
(OOP, comprehensions, *args / **kwargs , decorators, generators, complexity basics, common gotchas)
that were missing from the original course transcript. Everything is written for quick revision before
interviews/exams.
1. Introduction to Python
1.1 What is Python?
Python is both a language and an interpreter. Code is written in plain-text .py files and executed top-
to-bottom, left-to-right by the interpreter (CPython is the standard implementation). Python is: -
Interpreted (not compiled to machine code ahead of time — though CPython compiles to bytecode
internally, run via .pyc files). - Dynamically typed — you don’t declare variable types; they’re inferred
at runtime. - Strongly typed — Python won’t silently convert "2" + 2 (raises TypeError ).
1.2 Running Code
python [Link]
python3 [Link] # on systems where python2 also exists
Interactive shell: python or python3 with no filename (REPL — Read-Eval-Print Loop).
1.3 Hello, World
print("Hello, World")
Key Concepts: - Function: reusable block performing a task. print is built-in. - Argument: input
passed inside parentheses. - Side effect: observable action (like printing) with no returned value.
1.4 Errors
Syntax Error: violates grammar rules (unclosed bracket/quote) — program won’t run at all.
Runtime Error / Exception: occurs while running (e.g., dividing by zero).
Logical Error: program runs but gives wrong output — hardest to catch, needs testing.
2. Variables, Input/Output
2.1 Input
input() always returns a string, even if the user types a number.
name = input("What's your name? ")
2.2 Variables
= is the assignment operator (copies value right → left), not equality.
Python variables are references/labels pointing to objects in memory — not fixed-size boxes like in
C.
Naming rules: letters, digits, underscore; cannot start with a digit; case-sensitive; avoid reserved
keywords ( if , for , class , etc.).
2.3 Combining Strings and Variables
print("Hello, " + name) # concatenation (must be str + str)
print("Hello,", name) # print() adds a space between args automatically
print(f"Hello, {name}") # f-string (preferred, cleanest, allows expressions: f"{2+2}")
2.4 print Named Parameters
print("Hello", end="") # end: what to print after (default "\n")
print("a", "b", sep="-") # sep: separator between args (default " ") -> a-b
3. Strings and String Methods
3.1 Strings are Immutable
A str cannot be changed in place. Every method ( .upper() , .strip() , etc.) returns a new string; the
original is untouched.
s = "hello"
[Link]() # returns "HELLO" but does NOT change s
s = [Link]() # now s is "HELLO"
3.2 Common Methods
Method Effect
strip() remove leading/trailing whitespace
lstrip() / rstrip() strip only left / right side
capitalize() capitalize first character only
title() capitalize first letter of every word
upper() / lower() change case
split(delim) split into a list
opposite of split — join list into a string: "-
join(list)
".join(["a","b"]) → "a-b"
replace(old,new) replace substring
find(sub) index of substring, or -1 if not found
startswith() / endswith() boolean check
isdigit() / isalpha() / isalnum() boolean type checks
Method chaining works because each method returns a new string:
name = input("What's your name? ").strip().title()
3.3 Unpacking and Slicing
first, last = "David Malan".split(" ")
Slicing (missing from original notes — very commonly tested):
s = "Hello, World"
s[0] # 'H' -> indexing
s[0:5] # 'Hello' -> slice [start:stop) stop excluded
s[:5] # 'Hello' -> start defaults to 0
s[7:] # 'World' -> stop defaults to end
s[::-1] # 'dlroW ,olleH' -> reversed string, step = -1
s[-1] # last character
len(s) # length of string
3.4 Escape Characters
\" / \' — quotes inside string
\n — newline
\t — tab
\\ — literal backslash
3.5 String Formatting (extra, common in interviews)
"{} is {}".format(name, age) # old-style .format()
"%s is %d" % (name, age) # very old %-formatting
f"{name} is {age}" # modern, preferred
f"{3.14159:.2f}" # '3.14' — 2 decimal places
f"{1000000:,}" # '1,000,000' — comma separator
4. Numbers
4.1 Types
int — whole numbers, arbitrary precision (Python ints don’t overflow like in C/Java).
float — decimal numbers, subject to floating-point precision issues ( 0.1 + 0.2 != 0.3 exactly).
complex — numbers like 3+4j (rarely tested but exists).
4.2 Operators
Operator Meaning
+ - * / standard arithmetic; / always returns a float
// floor (integer) division — 7 // 2 = 3
% modulo — remainder
** exponentiation — 2 ** 3 = 8
4.3 Type Conversion
x = int(input("x: "))
y = float(input("y: "))
z = round(x + y) # round() rounds to nearest int (or n decimals: round(x, 2))
int("3.5") → raises ValueError (can’t parse a float string directly as int — must go via float() first:
int(float("3.5")) ).
4.4 Useful Built-ins
abs(x) , min(a,b,...) , max(a,b,...) , pow(x,y) , divmod(a,b) → returns (quotient, remainder) tuple.
5. Functions
5.1 Defining Functions
def hello(to="world"):
print(f"Hello, {to}")
hello() # "Hello, world" (uses default)
hello("David") # "Hello, David"
hello(to="Ron") # keyword argument
Parameters: names in the function definition.
Arguments: actual values passed when calling.
Default values must come after non-default parameters in the signature.
5.2 Return Values
def square(n):
return n * n
A function without an explicit return returns None implicitly.
5.3 *args and **kwargs (missing — frequently asked in interviews)
def total(*args): # args becomes a tuple of positional arguments
return sum(args)
total(1, 2, 3) # 6
def describe(**kwargs): # kwargs becomes a dict of keyword arguments
for k, v in [Link]():
print(k, v)
describe(name="Ron", age=17)
Order in a signature: def f(pos, *args, default=1, **kwargs):
5.4 main() Convention
Define a main() function for top-level logic to keep the file organized, and call it at the bottom.
5.5 Scope
Local variable: exists only inside the function it’s defined in.
Global variable: defined at module level, readable anywhere, but to modify a global inside a
function you must declare global var_name first (otherwise Python creates a new local variable
instead).
count = 0
def increment():
global count
count += 1
Avoid relying on globals — pass values as arguments/return values instead (cleaner, more
testable).
5.6 Function Definition Order & the __name__ Guard
Python reads top-to-bottom — a function must be defined before it’s called (though the body can
reference names defined later, since the body only runs when called). Using main() at the bottom
sidesteps ordering issues.
def main():
print(hello("World"))
def hello(name):
return f"Hello, {name}"
if __name__ == "__main__":
main()
__name__ is "__main__" when the file is run directly, and the module’s own name when imported — this
guard prevents code from auto-running on import.
5.7 Lambda Functions
An anonymous, single-expression function:
square = lambda x: x * x
square(5) # 25
Commonly used inline with sorted() , map() , filter() .
5.8 map , filter , reduce (missing — common interview topic)
nums = [1, 2, 3, 4]
squares = list(map(lambda x: x**2, nums)) # [1, 4, 9, 16]
evens = list(filter(lambda x: x % 2 == 0, nums)) # [2, 4]
from functools import reduce
total = reduce(lambda a, b: a + b, nums) # 10 (cumulative)
5.9 Recursion (missing — very common interview topic)
A function calling itself, with a base case to stop it.
def factorial(n):
if n == 0: # base case
return 1
return n * factorial(n - 1) # recursive case
Every recursive function needs: (1) a base case, (2) progress toward it each call. Without these →
RecursionError (stack overflow, Python’s default recursion limit is ~1000).
6. Conditionals and Boolean Logic
6.1 Comparison Operators
> < >= <= == != — remember = is assignment, == is comparison.
6.2 if / elif / else
if x < y:
print("x is less than y")
elif x > y:
print("x is greater than y")
else:
print("x is equal to y")
if/elif/else chains are mutually exclusive — only one branch executes, unlike separate if
statements which each get checked independently.
6.3 Logical Operators
and , or , not . Python supports chained comparisons: if 90 <= score <= 100:
6.4 Truthiness (missing — commonly tested)
In a boolean context, these are all falsy: False , 0 , 0.0 , "" (empty string), [] , {} , () , None .
Everything else is truthy.
if my_list: # true only if list is non-empty
print("has items")
6.5 is vs == (missing — very common interview question)
== compares values.
is compares identity (whether both names point to the same object in memory).
a = [1, 2, 3]
b = [1, 2, 3]
a == b # True (same values)
a is b # False (different objects in memory)
a is a # True
Use is mainly for None checks: if x is None: (preferred over x == None ).
6.6 match Statements (Python 3.10+)
match name:
case "Harry" | "Hermione" | "Ron":
print("Gryffindor")
case "Draco":
print("Slytherin")
case _:
print("Who?")
7. Loops and Data Structures
7.1 while Loops
i = 0
while i < 3:
print("Meow")
i += 1
break exits the loop entirely; continue skips to the next iteration.
7.2 for Loops and range()
for _ in range(3):
print("Meow")
range(5) # 0,1,2,3,4
range(2, 5) # 2,3,4
range(0, 10, 2) # 0,2,4,6,8 (start, stop, step)
7.3 Lists (Mutable, Ordered)
students = ["Hermione", "Harry", "Ron"]
[Link]("Draco") # add to end
[Link](0, "Neville")# insert at index
[Link]("Ron") # remove by value (first match)
[Link]() # remove & return last item (or pop(i) for index)
[Link](0)
len(students)
sorted(students) # returns new sorted list, original unchanged
[Link]() # sorts in place, returns None
[Link]()
"Harry" in students # membership test -> True/False
Zero-indexed. Negative indices count from the end ( students[-1] is last item).
7.4 List Comprehensions (missing — VERY frequently asked in interviews)
A concise way to build lists:
squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0] # with filter condition
pairs = [(x, y) for x in range(3) for y in range(3)] # nested loops
Equivalent longhand:
squares = []
for x in range(10):
[Link](x**2)
Dict and set comprehensions also exist:
squares_dict = {x: x**2 for x in range(5)}
unique = {x % 3 for x in range(10)} # set comprehension
7.5 Tuples (missing — Immutable, Ordered)
point = (3, 4)
x, y = point # unpacking
Tuples cannot be modified after creation (no .append() ); use when data shouldn’t change. Faster and
more memory-efficient than lists.
7.6 Sets (missing — Unordered, Unique Elements)
s = {1, 2, 3}
[Link](4)
[Link](2)
a = {1, 2, 3}
b = {2, 3, 4}
a | b # union -> {1,2,3,4}
a & b # intersection -> {2,3}
a - b # difference -> {1}
Good for de-duplication and fast membership checks ( in is O(1) average for sets vs O(n) for lists).
7.7 Dictionaries
students = {"Hermione": "Gryffindor", "Draco": "Slytherin"}
students["Hermione"] # access by key -> KeyError if missing
[Link]("Luna", "Unknown") # safe access with default, no error
students["Luna"] = "Ravenclaw" # add/update
del students["Draco"] # remove
"Hermione" in students # checks KEYS by default
[Link]()
[Link]()
[Link]() # key-value pairs, useful for looping
for name, house in [Link]():
print(name, house)
7.8 Mutable vs Immutable (missing — extremely common interview question)
Mutable (can change in place): list , dict , set .
Immutable (cannot change in place): str , int , float , bool , tuple .
Gotcha — mutable default arguments:
def add_item(item, lst=[]): # BAD: default list is created once and reused across calls
[Link](item)
return lst
Correct version:
def add_item(item, lst=None):
if lst is None:
lst = []
[Link](item)
return lst
Shallow vs deep copy:
import copy
a = [[1, 2], [3, 4]]
b = [Link]() # shallow copy: inner lists still shared
c = [Link](a) # deep copy: fully independent
7.9 Nested Loops and Nested Data
def print_square(size):
for i in range(size):
for j in range(size):
print("#", end="")
print()
List of dictionaries (common in real data / JSON):
students = [
{"name": "Harry", "house": "Gryffindor"},
{"name": "Draco", "house": "Slytherin"}
]
for student in students:
print(f"{student['name']} is in {student['house']}")
7.10 Input Validation Loop Pattern
while True:
n = int(input("What's n? "))
if n > 0:
break
8. Exception Handling
8.1 Errors vs Exceptions
Syntax Error: caught before the program runs at all.
Exception (runtime): ValueError , TypeError , NameError , IndexError , KeyError ,
ZeroDivisionError , FileNotFoundError , AttributeError .
8.2 try / except / else / finally
try:
x = int(input("x: "))
except ValueError:
print("x is not an integer")
else:
print(f"x is {x}") # runs only if try succeeded
finally:
print("Done") # ALWAYS runs (missing from original notes) — used for cleanup
(closing files/connections)
8.3 Catching Multiple / Generic Exceptions
try:
...
except (ValueError, TypeError) as e:
print(f"Error: {e}")
except Exception as e: # catch-all — use sparingly, hides bugs if overused
print(f"Unexpected: {e}")
8.4 Reprompting Pattern
def get_int(prompt):
while True:
try:
return int(input(prompt))
except ValueError:
pass
return exits the loop and function simultaneously.
pass = do nothing (a no-op placeholder).
8.5 Raising Your Own Exceptions (missing — commonly asked)
def withdraw(balance, amount):
if amount > balance:
raise ValueError("Insufficient funds")
return balance - amount
Custom exception classes:
class InsufficientFundsError(Exception):
pass
raise InsufficientFundsError("Not enough balance")
8.6 Best Practice
Keep try blocks small — wrap only the line(s) that can actually fail, so you don’t accidentally swallow
unrelated bugs.
9. Libraries and Modules
9.1 Importing
import random
[Link](["heads", "tails"])
from random import choice
choice(["heads", "tails"])
import random as r # aliasing (missing) — common with pandas as pd, numpy as np
9.2 Useful Standard Library Modules
random : [Link](seq) , [Link](a,b) (inclusive both ends), [Link](list) ,
[Link](seq, k) (k unique picks).
sys : [Link] (list of command-line args, argv[0] is script name), [Link]("msg") .
os (missing): [Link]() , [Link]() , [Link](path) , [Link](a, b) (safe path building
across OSes).
datetime (missing): [Link]() , [Link]() , formatting via .strftime("%Y-%m-
%d") .
math (missing): [Link]() , [Link]() , [Link]() , [Link] , [Link] .
collections (missing, useful for interviews): Counter (frequency counting), defaultdict (dict with
default factory), deque (fast append/pop from both ends — better than list for queues).
from collections import Counter
Counter("mississippi") # Counter({'i': 4, 's': 4, 'p': 2, 'm': 1})
9.3 Command-Line Arguments
import sys
if len([Link]) != 2:
[Link]("Usage: python [Link] NAME")
print(f"Hello, {[Link][1]}")
for arg in [Link][1:]: # slice skips program name
print(arg)
9.4 Third-Party Packages (pip)
pip install requests
import requests
response = [Link]("[Link]
data = [Link]() # JSON -> Python dict/list automatically
Virtual environments (missing — important for real projects/interviews):
python -m venv venv
source venv/bin/activate # Linux/Mac
venv\Scripts\activate # Windows
pip install -r [Link]
pip freeze > [Link]
9.5 Creating Your Own Modules
# [Link]
def hello(name):
return f"Hello, {name}"
# [Link]
from sayings import hello
print(hello("Ron"))
10. File I/O
10.1 Modes
"r" read (default), "w" write (overwrites), "a" append, "r+" read+write, add "b" for binary
(e.g. "rb" ).
10.2 Reading and Writing
with open("[Link]", "a") as file:
[Link](f"{name}\n")
with open("[Link]") as file:
for line in file:
print(f"Hello, {[Link]()}")
with open("[Link]") as file:
lines = [Link]() # list of all lines
content = [Link]() # OR: entire file as one string
with auto-closes the file even if an exception occurs — always prefer it over manual open() / close() .
10.3 CSV Files
import csv
with open("[Link]") as file:
reader = [Link](file)
for row in reader:
print(f"{row[0]} is in {row[1]}")
with open("[Link]") as file:
reader = [Link](file) # header row becomes dict keys
for row in reader:
print(f"{row['name']} is from {row['home']}")
with open("[Link]", "a", newline="") as file:
writer = [Link](file, fieldnames=["name", "home"])
[Link]({"name": name, "home": home})
10.4 JSON Files (missing — very commonly used)
import json
with open("[Link]") as file:
data = [Link](file) # JSON file -> Python object
with open("[Link]", "w") as file:
[Link](data, file, indent=2) # Python object -> JSON file
[Link](data) # object -> JSON string
[Link](text) # JSON string -> object
10.5 Sorting with key and lambda
students = [{"name": "Harry", "house": "Gryffindor"}, {"name": "Draco", "house": "Slytherin"}]
for s in sorted(students, key=lambda s: s["name"]):
print(s["name"])
sorted(students, key=lambda s: s["name"], reverse=True) # descending
11. Object-Oriented Programming (missing entirely — essential for
placements)
11.1 Classes and Objects
class Student:
def __init__(self, name, house): # constructor
[Link] = name
[Link] = house
def __str__(self): # controls print(object) output
return f"{[Link]} from {[Link]}"
hermione = Student("Hermione", "Gryffindor")
print(hermione) # uses __str__ -> "Hermione from Gryffindor"
self refers to the specific instance; always the first parameter of instance methods.
__init__ runs automatically when an object is created.
11.2 The Four Pillars of OOP
Concept Meaning
bundling data + methods together in a class; restricting
Encapsulation direct access via naming convention ( _protected ,
__private )
Abstraction hiding internal complexity, exposing only what’s needed
Inheritance a class reusing/extending another class’s behavior
same method name behaves differently depending on the
Polymorphism
object
11.3 Inheritance
class Person:
def __init__(self, name):
[Link] = name
def greet(self):
return f"Hi, I'm {[Link]}"
class Student(Person): # inherits from Person
def __init__(self, name, school):
super().__init__(name) # call parent constructor
[Link] = school
def greet(self): # method overriding (polymorphism)
return f"{super().greet()}, I study at {[Link]}"
11.4 Class Variables vs Instance Variables
class Dog:
species = "Canine" # class variable — shared by ALL instances
def __init__(self, name):
[Link] = name # instance variable — unique per object
11.5 Common Dunder (Magic) Methods
Method Purpose
__init__ constructor
__str__ human-readable string ( print(obj) )
developer-facing representation (shown in
__repr__
shell/debugger)
__len__ enables len(obj)
__eq__ enables obj1 == obj2 comparison
12. Generators and Iterators (missing — commonly asked)
12.1 Generators
A generator function uses yield instead of return , producing values lazily (one at a time), which saves
memory for large sequences.
def count_up_to(n):
i = 1
while i <= n:
yield i
i += 1
for num in count_up_to(5):
print(num)
Generator expression (like a list comprehension but lazy, parentheses instead of brackets):
squares = (x**2 for x in range(1000000)) # doesn't compute all at once
12.2 Iterators
Any object with __iter__ and __next__ methods. for loops call these under the hood. Lists, strings,
dicts are all iterable but not themselves iterators — iter() converts an iterable into an iterator, next()
advances it.
13. Decorators (missing — common interview topic)
A decorator wraps a function to add extra behavior without modifying its code.
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function runs")
result = func(*args, **kwargs)
print("After the function runs")
return result
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
Common built-in decorators: @staticmethod (method that doesn’t need self ), @classmethod (receives
the class cls instead of instance), @property (turns a method into a read-only attribute).
14. Unit Testing
14.1 Why Test
Unit tests verify individual functions work correctly, and continue working after future changes.
14.2 assert
def test_square():
assert square(2) == 4
assert square(-2) == 4
14.3 pytest
pip install pytest
pytest test_calculator.py
Conventions: file names test_*.py , function names test_*() .
14.4 Testing Exceptions
import pytest
def test_str():
with [Link](TypeError):
square("cat")
14.5 Designing for Testability
Functions that return values are easier to test than those that only print (side effects only, returns
None ).
14.6 Organizing Tests in Folders
Add an empty __init__.py in the test folder, then run pytest test/ .
15. Time & Space Complexity Basics (missing — asked in almost every
placement interview)
15.1 Big-O Notation
Describes how runtime/memory grows as input size ( n ) grows — worst-case, ignoring constants.
Complexity Name Example
O(1) Constant dict/set lookup, list indexing
O(log n) Logarithmic binary search
O(n) Linear single loop through a list
efficient sorting (merge sort,
O(n log n) Linearithmic
Python’s sorted() )
nested loops over the same list,
O(n²) Quadratic bubble sort
O(2ⁿ) Exponential naive recursive Fibonacci
15.2 Common Python Complexity Facts
List indexing/append: O(1). List insert(0, x) or remove(x) : O(n).
Dict/set lookup, insert, delete: O(1) average.
in on a list: O(n). in on a set/dict: O(1) average — prefer sets for membership checks.
sorted() / .sort() : O(n log n), uses Timsort.
16. Placement Interview — Quick Q&A Revision
Q: Difference between list and tuple? List is mutable, tuple is immutable; tuples are slightly faster
and hashable (can be dict keys/set elements), lists cannot be.
Q: Difference between is and == ? == checks value equality, is checks identity (same object in
memory).
Q: What is a mutable default argument bug? Default argument objects (like [] or {} ) are created
only once at function definition time and shared across all calls unless reset inside the function.
Q: What’s the difference between deepcopy and shallow copy ? Shallow copy duplicates the outer
container but nested objects are still shared references; deep copy recursively duplicates everything.
Q: What are *args and **kwargs used for? Allowing a function to accept a variable number of
positional ( *args → tuple) or keyword ( **kwargs → dict) arguments.
Q: Difference between __init__ and __new__ ? __new__ actually creates the object (rarely
overridden); __init__ initializes it after creation.
Q: What is a generator, and why use one? A function using yield that produces values lazily one at
a time — saves memory versus building a full list upfront.
Q: Why are strings immutable in Python? For memory efficiency (string interning/caching), thread-
safety, and to allow strings to be used as dict keys.
Q: How does Python manage memory? Reference counting plus a cyclic garbage collector to clean
up reference cycles that reference counting alone can’t catch.
Q: Difference between append() and extend() on a list? append(x) adds x as a single element;
extend(iterable) adds each element of the iterable individually.
[1,2].append([3,4]) # [1, 2, [3, 4]]
[1,2].extend([3,4]) # [1, 2, 3, 4]
Q: What does if __name__ == "__main__": do? Ensures a block of code only runs when the file is
executed directly, not when it’s imported as a module elsewhere.
Q: Difference between .pop() and del ? pop() removes and returns the value; del just removes it
(no return).
Q: What is PEP 8? Python’s official style guide (naming conventions, indentation, line length, etc.) —
commonly referenced in code-quality interview rounds.
Q: What’s the GIL (Global Interpreter Lock)? A mutex in CPython that allows only one thread to
execute Python bytecode at a time, which limits true multi-threaded CPU-bound parallelism (though I/O-
bound multithreading and multiprocessing are unaffected/available as workarounds).
17. Key Design Principles
1. Readability counts — prefer clear code over clever one-liners.
2. DRY (Don’t Repeat Yourself) — extract repeated logic into functions.
3. Mutually exclusive conditionals — use if/elif/else , not independent if s, when only one
branch should run.
4. Defensive programming — validate all user input; assume it will be wrong.
5. Small try blocks — wrap only the risky line(s).
6. Abstraction — break big problems into small, well-named functions.
7. Zero-indexing — lists/strings start at index 0.
8. Scope awareness — locals stay local unless returned or passed out.
9. The main guard — protect main() with if __name__ == "__main__": .
10. Test your code — write automated unit tests, especially for pure functions.
11. Prefer built-ins and comprehensions over manual loops where it improves clarity.
12. Know your data structure’s complexity — pick sets/dicts for lookups, lists for order, tuples for
fixed immutable records.
End of Notes — Good luck with your placements!