Python’s key features
- Python is dynamically typed, interpreted, and supports multiple programming paradigms (OOP, functional, procedural). It has a large standard library and automatic memory management.
Python memory management
- Python uses a private heap for memory management and has built-in garbage collection using reference counting and cyclic garbage collection.
Deep copy and shallow copy
- A shallow copy creates a new object but references the original elements, while a deep copy creates a completely independent copy of the original object and its elements.
Global Interpreter Lock (GIL)
- GIL is a mutex in CPython that allows only one thread to execute at a time, limiting true parallel execution in multi-threaded programs.
Python dynamic typing handling
- Python determines variable types at runtime, making development faster but potentially leading to runtime errors if types are misused.
CPython and Python’s different implementations
- CPython is the default interpreter, PyPy offers JIT compilation for speed, Jython runs on the JVM, and IronPython integrates with .NET.
- CPython is the default and most widely used implementation of Python, written in C. It compiles Python code into bytecode and executes it using a stack-based virtual machine. CPython includes a built-in garbage collector and follows the Global Interpreter Lock (GIL), which restricts true parallel execution in multi-threaded programs. It is the reference implementation of Python, meaning all other implementations (like PyPy, Jython, and IronPython) aim for compatibility with CPython.
Python’s exception handling
- Python uses
try-except-finally
blocks to handle runtime errors and prevent crashes. Exceptions help manage unexpected conditions like invalid input, file errors, or network failures.
✅ Basic Exception Handling
try:
x = 1 / 0 # Division by zero error
except ZeroDivisionError:
print("Cannot divide by zero")
✅ Catching Multiple Exceptions
try:
num = int(input("Enter a number: ")) # Can raise ValueError
result = 10 / num # Can raise ZeroDivisionError
except (ValueError, ZeroDivisionError) as e:
print(f"Error occurred: {e}")
✅ Using else
with try
(Only Runs if No Exception Occurs)
try:
file = open("data.txt", "r")
except FileNotFoundError:
print("File not found")
else:
print(file.read()) # Runs only if no exception occurs
✅ finally
Block (Always Executes)
try:
file = open("data.txt", "r")
finally:
file.close() # Ensures the file is closed no matter what
✅ Raising Custom Exceptions
def check_age(age):
if age < 18:
raise ValueError("Age must be 18 or above")
return "Access granted"
print(check_age(15)) # Raises ValueError
✅ Defining Custom Exception Classes
class CustomError(Exception):
"""Custom exception example."""
pass
try:
raise CustomError("Something went wrong!")
except CustomError as e:
print(e)
✅ Best Practices
- Catch specific exceptions (avoid generic except Exception:).
- Use finally for resource cleanup (closing files, database connections).
- Keep exception handling lightweight (avoid swallowing errors).
- Log errors properly for debugging instead of printing (logging module).
Python’s logging Module
Python’s logging
module provides structured logging with different levels (DEBUG
, INFO
, WARNING
, ERROR
, CRITICAL
) for debugging and monitoring.
✅ Basic Logging
import logging
logging.basicConfig(level=logging.DEBUG) # Set log level
logging.debug("Debugging info")
logging.info("General info")
logging.warning("Warning!")
logging.error("Error occurred")
logging.critical("Critical issue")
✅ Logging to a File
logging.basicConfig(filename="app.log", level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s")
logging.info("Application started")
✅ Logging Exceptions
try:
1 / 0
except ZeroDivisionError:
logging.error("Exception occurred", exc_info=True)
✅ Best Practices
- Use log levels (DEBUG, INFO, etc.).
- Avoid print() in production.
- Write logs to files for debugging.
- Use structured logs with timestamps & file names.
- The logging module improves debugging & production monitoring!
Python Execution Model
- Python code is first compiled into bytecode (.pyc files), which is then executed by the Python Virtual Machine (PVM). The PVM interprets bytecode and interacts with the system to execute the program. (source code → bytecode → interpreter)
Python Standard Library & Built-in Modules
Python comes with a rich standard library that provides built-in modules for common tasks:
- sys – System-specific parameters and functions (sys.argv, sys.exit())
- os – Interacting with the operating system (os.path, os.environ)
- math – Mathematical operations (math.sqrt(), math.pi)
- random – Random number generation (random.randint(), random.choice())
- datetime – Handling dates and times (datetime.datetime.now(), timedelta)
Using pip to Install Packages
Python uses pip (Python Package Installer) to manage third-party libraries. Install packages with:
pip install package_name
Upgrade packages:
pip install --upgrade package_name
List installed packages:
pip list
Use virtual environments (venv
) to keep dependencies isolated.
Importance of PEP 8 (Python Style Guide)
PEP 8 is Python’s official style guide that ensures readability and consistency. Key rules:
- Use 4 spaces per indentation
- Keep line length ≤ 79 characters
- Use meaningful variable names
- Follow snake_case for functions and variables
- Add docstrings and comments
- Use flake8 or black for automatic PEP 8 checks.
Using Docstrings and Comments
- Comments (#) explain code but are ignored at runtime.
- Docstrings (""" """) provide documentation inside functions, classes, and modules.
# This is a comment
def add(x, y):
"""Returns the sum of x and y."""
return x + y
# Use help(add) to view the function docstring.
Unicode Handling (str vs bytes)
-
str
: Represents Unicode text (default in Python 3). -
bytes
: Represents binary data (e.g., encoded strings, files, network data).
text = "hello" # Unicode string
binary = b"hello" # Byte string
encoded = text.encode("utf-8") # Convert str → bytes
decoded = encoded.decode("utf-8") # Convert bytes → str
Integer Division Behavior (/ vs //)
-
/
(True division): Always returns a float (even for integers). -
//
(Floor division): Discards the decimal, returns an integer.
print(5 / 2) # 2.5
print(4 / 2) # 2.0
print(5 // 2) # 2
print(4 // 2) # 2
Brief Mention of asyncio
Python’s asyncio
module enables asynchronous programming, allowing tasks to run concurrently without blocking execution. It is useful for I/O-bound operations like network requests, file handling, and database queries.
Example:
import asyncio
async def say_hello():
await asyncio.sleep(1)
print("Hello, Async!")
asyncio.run(say_hello())
- Key Concepts:
async def
,await
,event loop
,asyncio.run()
. - Use Cases: Web scraping, API calls, chat servers, background tasks.
- Not a replacement for threading/multiprocessing but great for non-blocking I/O.
How Python Supports Concurrency Despite GIL
Python’s Global Interpreter Lock (GIL) prevents multiple threads from executing Python bytecode simultaneously in CPython, limiting true parallelism in multi-threaded programs.
However, Python still supports concurrency through:
- Multi-threading (
threading
module) – Suitable for I/O-bound tasks (e.g., API calls, file operations) since GIL releases the lock during I/O. - Multi-processing (
multiprocessing
module) – Bypasses GIL by using separate processes, allowing true parallel execution for CPU-bound tasks. - Asynchronous programming (
asyncio
) – Uses an event loop to run non-blocking tasks concurrently without creating multiple threads or processes. Example: Multiprocessing for True Parallelism
from multiprocessing import Pool
def square(n):
return n * n
with Pool(processes=4) as pool:
print(pool.map(square, [1, 2, 3, 4])) # Runs in parallel
✅ Use threads for I/O-bound tasks and multiprocessing for CPU-heavy computations.
with
Statement & Context Managers in Python
The with
statement is used to manage resources efficiently by ensuring proper setup and cleanup, even in case of errors. It automatically calls __enter__()
when entering the block and __exit__()
when exiting.
✅ Using with for File Handling (Auto-Close Files)
with open("data.txt", "r") as file:
content = file.read() # No need for `file.close()`
✔ Ensures the file closes automatically after exiting the block.
✅ Using with for Database Connections
import sqlite3
with sqlite3.connect("mydb.sqlite") as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
✔ No need to manually close the connection.
✅ Using with for Locking Threads
from threading import Lock
lock = Lock()
with lock:
# Critical section (automatically releases lock)
print("Thread-safe operation")
✔ Ensures the lock is released properly after execution.
✅ Creating a Custom Context Manager (__enter__()
& __exit__()
)
class ManagedResource:
def __enter__(self):
print("Resource acquired")
return self # Object returned inside `with`
def __exit__(self, exc_type, exc_value, traceback):
print("Resource released")
with ManagedResource() as res:
print("Using resource")
✔ Manages resource allocation and cleanup automatically.
✅ Best Practices
- Use with whenever working with files, database connections, locks, network sockets.
- Prevents resource leaks and improves readability.
- Prefer built-in context managers (open(), threading.Lock(), sqlite3.connect()) when available.
- Using
with
ensures reliable resource management in Python!
Best Practices & Common Beginner Pitfalls in Python
✅ Best Practices
- Follow PEP 8 for Clean Code Use meaningful variable names, proper indentation, and consistent formatting.
# ✅ Good
def calculate_area(radius):
return 3.14 * radius ** 2
# ❌ Bad
def ca(r): return 3.14*r*r
- Use Virtual Environments (
venv
) Always create isolated environments to manage dependencies and avoid conflicts.
python -m venv myenv
source myenv/bin/activate # (Linux/macOS)
myenv\Scripts\activate # (Windows)
- Use
get()
to Avoid KeyErrors in Dictionaries
data = {"name": "Alice"}
print(data.get("age", "Unknown")) # Instead of data["age"]
- Use List Comprehensions for Cleaner Code
squared = [x**2 for x in range(10)]
- Use
with
for File Handling (Auto Close Files)
with open("file.txt", "r") as f:
content = f.read()
- Use
enumerate()
Instead of Manual Indexing
for i, item in enumerate(["a", "b", "c"]):
print(i, item)
- Use
zip()
to Iterate Multiple Sequences Simultaneously
names = ["Alice", "Bob"]
scores = [85, 90]
for name, score in zip(names, scores):
print(name, score)
❌ Common Beginner Pitfalls
❌ Modifying a List While Iterating
# Wrong
nums = [1, 2, 3, 4]
for num in nums:
if num % 2 == 0:
nums.remove(num) # ❌ Unexpected behavior
# Correct
nums = [num for num in nums if num % 2 != 0]
❌ Using Mutable Default Arguments in Functions
# Wrong (Mutable default argument)
def add_item(item, lst=[]):
lst.append(item)
return lst
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] ❌ Unexpected!
# Correct
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst
❌ Not Handling Exceptions Properly
# Wrong
value = int(input("Enter a number: ")) # Crashes on invalid input
# Correct
try:
value = int(input("Enter a number: "))
except ValueError:
print("Invalid input!")
❌ Misusing is
Instead of ==
for Comparisons
x = 1000
print(x == 1000) # ✅ True (Correct)
print(x is 1000) # ❌ May be False (Don't use `is` for value comparisons)
✅ Final Tips:
- Use
type hints
for better readability (def greet(name: str) -> str:
). - Avoid global variables inside functions.
- Learn iterators, generators, and functional programming early.
- Profile and optimize performance only when necessary.
Top comments (0)