๐Ÿ Python Interview Guide

Python Basics & Features

What is Python and what are its key features?

Python is a high-level, interpreted, object-oriented programming language known for its simplicity and readability.

Key Features of Python:

What is PEP 8 and why is it important?

PEP 8 is the official style guide for Python code. It provides conventions for writing readable and consistent Python code.

Key PEP 8 Guidelines:

# Good PEP 8 style
def calculate_average(numbers):
    """Calculate the average of a list of numbers."""
    if not numbers:
        return 0
    return sum(numbers) / len(numbers)

class StudentGradeCalculator:
    def __init__(self, student_name):
        self.student_name = student_name
        self.grades = []

Data Types & Variables

What are Python's built-in data types?

Numeric Types:

Type Description Example Methods
int Integer numbers 42, -17, 0 bit_length(), to_bytes()
float Floating-point numbers 3.14, -0.5, 2e10 is_integer(), hex()
complex Complex numbers 3+4j, 2-1j real, imag, conjugate()

Sequence Types:

Collection Types:

# Type examples
integer_num = 42
float_num = 3.14159
complex_num = 3 + 4j
string_text = "Hello, Python!"
list_data = [1, 2, 3, "mixed", True]
tuple_data = (1, 2, 3)
dict_data = {"name": "John", "age": 30}
set_data = {1, 2, 3, 3}  # {1, 2, 3}

# Type checking
print(type(integer_num))  # 
print(isinstance(float_num, (int, float)))  # True

Mutable vs Immutable Objects

Immutable Objects:

Cannot be changed after creation. Examples: int, float, str, tuple, frozenset

Mutable Objects:

Can be modified after creation. Examples: list, dict, set, user-defined objects

# Immutable example
s1 = "Hello"
s2 = s1
s1 += " World"  # Creates new string
print(s1)  # "Hello World"
print(s2)  # "Hello" (unchanged)

# Mutable example
list1 = [1, 2, 3]
list2 = list1
list1.append(4)  # Modifies existing list
print(list1)  # [1, 2, 3, 4]
print(list2)  # [1, 2, 3, 4] (same object)

Data Structures

What are the main data structures in Python?

1. Lists

Ordered, mutable collection that allows duplicates.

# List operations
fruits = ["apple", "banana", "cherry"]
fruits.append("orange")        # Add to end
fruits.insert(1, "grape")      # Insert at position
fruits.remove("banana")        # Remove by value
popped = fruits.pop()          # Remove and return last
fruits[0] = "kiwi"             # Modify by index

# List comprehension
squares = [x**2 for x in range(10)]
even_squares = [x**2 for x in range(10) if x % 2 == 0]

2. Dictionaries

Unordered collection of key-value pairs.

# Dictionary operations
person = {"name": "John", "age": 30, "city": "New York"}
person["job"] = "Developer"     # Add new key-value
age = person.get("age", 0)      # Safe access with default
person.update({"salary": 75000, "age": 31})  # Update multiple

# Dictionary comprehension
word_counts = {word: len(word) for word in ["hello", "world", "python"]}

# Useful methods
keys = list(person.keys())      # Get all keys
values = list(person.values())  # Get all values
items = list(person.items())    # Get key-value pairs

3. Sets

Unordered collection of unique elements.

# Set operations
set1 = {1, 2, 3, 4, 5}
set2 = {4, 5, 6, 7, 8}

union = set1 | set2           # {1, 2, 3, 4, 5, 6, 7, 8}
intersection = set1 & set2     # {4, 5}
difference = set1 - set2       # {1, 2, 3}
symmetric_diff = set1 ^ set2   # {1, 2, 3, 6, 7, 8}

# Set methods
set1.add(6)                   # Add single element
set1.update([7, 8, 9])        # Add multiple elements
set1.discard(10)              # Remove if exists (no error)
set1.remove(1)                # Remove (raises KeyError if not found)

4. Tuples

Ordered, immutable collection.

# Tuple operations
coordinates = (10, 20)
person_info = ("John", 30, "Developer")

# Tuple unpacking
name, age, job = person_info
x, y = coordinates

# Named tuples (from collections)
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)
print(p.x, p.y)  # 10 20

Functions & Scope

How do functions work in Python?

Function Definition and Arguments:

# Basic function
def greet(name, greeting="Hello"):
    """Function with default parameter."""
    return f"{greeting}, {name}!"

# Variable arguments
def sum_all(*args):
    """Function with variable positional arguments."""
    return sum(args)

def print_info(**kwargs):
    """Function with variable keyword arguments."""
    for key, value in kwargs.items():
        print(f"{key}: {value}")

# Mixed parameters
def complex_function(required, default="value", *args, **kwargs):
    print(f"Required: {required}")
    print(f"Default: {default}")
    print(f"Args: {args}")
    print(f"Kwargs: {kwargs}")

# Usage examples
print(greet("Alice"))                    # Hello, Alice!
print(greet("Bob", "Hi"))               # Hi, Bob!
print(sum_all(1, 2, 3, 4, 5))          # 15
print_info(name="John", age=30)         # name: John, age: 30

Scope and Namespaces:

# LEGB Rule: Local -> Enclosing -> Global -> Built-in
global_var = "I'm global"

def outer_function():
    enclosing_var = "I'm in enclosing scope"
    
    def inner_function():
        local_var = "I'm local"
        print(local_var)        # Local
        print(enclosing_var)    # Enclosing
        print(global_var)       # Global
        print(len([1, 2, 3]))   # Built-in
    
    return inner_function

# Global and nonlocal keywords
counter = 0

def increment_global():
    global counter
    counter += 1

def create_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

Lambda Functions:

# Lambda syntax
square = lambda x: x**2
add = lambda x, y: x + y

# Common use with higher-order functions
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))
total = functools.reduce(lambda x, y: x + y, numbers)

# Sorting with lambda
students = [("Alice", 85), ("Bob", 90), ("Charlie", 78)]
students.sort(key=lambda x: x[1])  # Sort by grade

Decorators

What are decorators and how do they work?

Decorators are a way to modify or enhance functions/classes without permanently modifying their code. They use the @ symbol syntax.

Function Decorators:

# Basic decorator
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before function call")
        result = func(*args, **kwargs)
        print("After function call")
        return result
    return wrapper

@my_decorator
def say_hello(name):
    print(f"Hello, {name}!")
    return f"Greeting for {name}"

# Equivalent to: say_hello = my_decorator(say_hello)

# Decorator with parameters
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def print_message(message):
    print(message)

# Built-in decorators
class Calculator:
    def __init__(self, value):
        self._value = value
    
    @property
    def value(self):
        return self._value
    
    @value.setter
    def value(self, new_value):
        if new_value < 0:
            raise ValueError("Value must be non-negative")
        self._value = new_value
    
    @staticmethod
    def add(a, b):
        return a + b
    
    @classmethod
    def from_string(cls, value_str):
        return cls(int(value_str))

Classes & Objects

How do you create and use classes in Python?

Basic Class Definition:

class Person:
    # Class variable (shared by all instances)
    species = "Homo sapiens"
    
    def __init__(self, name, age):
        # Instance variables
        self.name = name
        self.age = age
        self._private_var = "This is private by convention"
    
    # Instance method
    def introduce(self):
        return f"Hi, I'm {self.name} and I'm {self.age} years old"
    
    # Property with getter/setter
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        if value < 0:
            raise ValueError("Age cannot be negative")
        self._age = value

# Creating instances
person1 = Person("Alice", 30)
person2 = Person("Bob", 25)

print(person1.introduce())  # Hi, I'm Alice and I'm 30 years old
print(Person.species)       # Homo sapiens

Special Methods (Magic Methods):

class BankAccount:
    def __init__(self, account_number, balance=0):
        self.account_number = account_number
        self.balance = balance
    
    def __str__(self):
        return f"Account {self.account_number}: ${self.balance:.2f}"
    
    def __repr__(self):
        return f"BankAccount('{self.account_number}', {self.balance})"
    
    def __eq__(self, other):
        if isinstance(other, BankAccount):
            return self.account_number == other.account_number
        return False
    
    def __len__(self):
        return len(self.account_number)
    
    def __bool__(self):
        return self.balance > 0

# Usage
account = BankAccount("123456", 1000)
print(str(account))    # Account 123456: $1000.00
print(len(account))    # 6
print(bool(account))   # True

Memory Management

How does Python manage memory?

Python uses automatic memory management with reference counting and garbage collection.

Reference Counting:

import sys

# Object creation increases reference count
my_list = [1, 2, 3]
print(sys.getrefcount(my_list))  # Reference count

# Multiple references
another_ref = my_list
print(sys.getrefcount(my_list))  # Increased count

# Deleting reference
del another_ref
print(sys.getrefcount(my_list))  # Decreased count

Garbage Collection:

import gc

# Check garbage collection statistics
print(gc.get_stats())

# Manual garbage collection
collected = gc.collect()
print(f"Collected {collected} objects")

# Disable/enable automatic garbage collection
gc.disable()
gc.enable()

# Memory optimization tips
class OptimizedClass:
    __slots__ = ['x', 'y']  # Reduces memory usage
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

Memory Profiling:

# Using memory_profiler
from memory_profiler import profile

@profile
def memory_intensive_function():
    # Create large data structures
    big_list = [i for i in range(1000000)]
    big_dict = {i: i**2 for i in range(100000)}
    return big_list, big_dict

# Using tracemalloc (built-in)
import tracemalloc

tracemalloc.start()
# ... your code here ...
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.1f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.1f} MB")
tracemalloc.stop()

Global Interpreter Lock (GIL)

What is the GIL and how does it affect Python programs?

The GIL is a mutex that protects access to Python objects, preventing multiple native threads from executing Python bytecodes simultaneously.

GIL Impact:

Working with Threading:

import threading
import time

def io_bound_task(name, duration):
    print(f"Starting {name}")
    time.sleep(duration)  # Simulates I/O operation (GIL released)
    print(f"Finished {name}")

def cpu_bound_task(name, iterations):
    print(f"Starting {name}")
    total = 0
    for i in range(iterations):
        total += i * i
    print(f"Finished {name}, result: {total}")

# Threading for I/O-bound tasks
threads = []
for i in range(3):
    t = threading.Thread(target=io_bound_task, args=(f"Task-{i}", 2))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

Alternatives to GIL:

# Multiprocessing for CPU-bound tasks
from multiprocessing import Process, Pool
import os

def cpu_intensive_work(n):
    return sum(i * i for i in range(n))

if __name__ == '__main__':
    # Using Process
    processes = []
    for i in range(os.cpu_count()):
        p = Process(target=cpu_intensive_work, args=(1000000,))
        processes.append(p)
        p.start()
    
    for p in processes:
        p.join()
    
    # Using Pool
    with Pool() as pool:
        results = pool.map(cpu_intensive_work, [1000000] * 4)
        print(results)

Django Framework

What is Django and its key features?

Django is a high-level Python web framework that follows the Model-View-Template (MVT) pattern.

Django Architecture (MVT):

Django Models:

# models.py
from django.db import models
from django.contrib.auth.models import User

class Category(models.Model):
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.name

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        ordering = ['-created_at']
    
    def __str__(self):
        return self.title

Django Views:

# views.py
from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse
from django.views.generic import ListView, CreateView
from .models import Post

# Function-based view
def post_list(request):
    posts = Post.objects.all().select_related('author', 'category')
    return render(request, 'blog/post_list.html', {'posts': posts})

def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    return render(request, 'blog/post_detail.html', {'post': post})

# Class-based view
class PostListView(ListView):
    model = Post
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 10
    
    def get_queryset(self):
        return Post.objects.select_related('author', 'category')

# API view
from django.views.decorators.csrf import csrf_exempt
import json

@csrf_exempt
def api_posts(request):
    if request.method == 'GET':
        posts = list(Post.objects.values('id', 'title', 'content'))
        return JsonResponse({'posts': posts})
    
    elif request.method == 'POST':
        data = json.loads(request.body)
        post = Post.objects.create(
            title=data['title'],
            content=data['content'],
            author=request.user
        )
        return JsonResponse({'id': post.id, 'status': 'created'})

Flask Framework

What is Flask and how does it differ from Django?

Flask is a lightweight, micro web framework for Python. It's more minimalist and flexible compared to Django.

Basic Flask Application:

from flask import Flask, request, jsonify, render_template
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
db = SQLAlchemy(app)

# Model
class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    content = db.Column(db.Text, nullable=False)
    
    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'content': self.content
        }

# Routes
@app.route('/')
def home():
    posts = Post.query.all()
    return render_template('index.html', posts=posts)

@app.route('/api/posts', methods=['GET', 'POST'])
def api_posts():
    if request.method == 'GET':
        posts = Post.query.all()
        return jsonify([post.to_dict() for post in posts])
    
    elif request.method == 'POST':
        data = request.get_json()
        post = Post(title=data['title'], content=data['content'])
        db.session.add(post)
        db.session.commit()
        return jsonify(post.to_dict()), 201

@app.route('/api/posts/', methods=['GET', 'PUT', 'DELETE'])
def api_post_detail(post_id):
    post = Post.query.get_or_404(post_id)
    
    if request.method == 'GET':
        return jsonify(post.to_dict())
    
    elif request.method == 'PUT':
        data = request.get_json()
        post.title = data.get('title', post.title)
        post.content = data.get('content', post.content)
        db.session.commit()
        return jsonify(post.to_dict())
    
    elif request.method == 'DELETE':
        db.session.delete(post)
        db.session.commit()
        return '', 204

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(debug=True)

Flask Extensions:

NumPy & Pandas

What are NumPy and Pandas used for?

NumPy (Numerical Python):

Foundation for scientific computing in Python, providing support for large multi-dimensional arrays.

import numpy as np

# Creating arrays
arr1 = np.array([1, 2, 3, 4, 5])
arr2 = np.zeros((3, 4))
arr3 = np.ones((2, 3))
arr4 = np.arange(0, 10, 2)  # [0, 2, 4, 6, 8]

# Array operations
matrix = np.array([[1, 2], [3, 4]])
print(matrix.shape)    # (2, 2)
print(matrix.dtype)    # int64

# Mathematical operations
result = np.dot(matrix, matrix)  # Matrix multiplication
mean_val = np.mean(arr1)
std_val = np.std(arr1)

Pandas (Data Analysis):

High-level data manipulation and analysis library built on NumPy.

import pandas as pd

# Creating DataFrames
data = {
    'Name': ['Alice', 'Bob', 'Charlie', 'Diana'],
    'Age': [25, 30, 35, 28],
    'City': ['New York', 'London', 'Tokyo', 'Paris']
}
df = pd.DataFrame(data)

# Data operations
print(df.head())
print(df.info())
print(df.describe())

# Filtering and selection
young_people = df[df['Age'] < 30]
names_ages = df[['Name', 'Age']]

# Grouping and aggregation
grouped = df.groupby('City')['Age'].mean()

# File I/O
df.to_csv('people.csv', index=False)
df_loaded = pd.read_csv('people.csv')

Exception Handling

How do you handle exceptions in Python?

Basic Exception Handling:

try:
    # Code that might raise an exception
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Division by zero error: {e}")
except Exception as e:
    print(f"An unexpected error occurred: {e}")
else:
    print("No exceptions occurred")
finally:
    print("This always executes")

# Multiple exception types
try:
    value = int(input("Enter a number: "))
    result = 10 / value
except (ValueError, ZeroDivisionError) as e:
    print(f"Error: {type(e).__name__}: {e}")

# Custom exceptions
class CustomError(Exception):
    def __init__(self, message, code=None):
        self.message = message
        self.code = code
        super().__init__(self.message)

def validate_age(age):
    if age < 0:
        raise CustomError("Age cannot be negative", code="NEGATIVE_AGE")
    if age > 150:
        raise CustomError("Age seems unrealistic", code="UNREALISTIC_AGE")
    return True

try:
    validate_age(-5)
except CustomError as e:
    print(f"Validation error: {e.message} (Code: {e.code})")

Python Best Practices

What are some Python coding best practices?

Code Quality Tools:

# Type hints example
from typing import List, Dict, Optional

def process_data(items: List[str], 
                config: Dict[str, any]) -> Optional[List[int]]:
    """Process a list of items according to configuration.
    
    Args:
        items: List of string items to process
        config: Configuration dictionary
        
    Returns:
        List of processed integers or None if processing fails
    """
    if not items:
        return None
    
    try:
        return [len(item) for item in items if item.strip()]
    except Exception as e:
        print(f"Processing failed: {e}")
        return None

# Using dataclasses for cleaner code
from dataclasses import dataclass

@dataclass
class Person:
    name: str
    age: int
    email: Optional[str] = None
    
    def is_adult(self) -> bool:
        return self.age >= 18