Skip to content

Blog

Welcome to my blog where I share practical insights, in-depth tutorials, and real-world experiences in software engineering. As a full-stack developer specializing in Python and system design, I write about the challenges, solutions, and best practices I encounter building scalable applications.

A Grateful Student on Teacher's Day

Today, September 5th, is celebrated as Teacher's Day in India, in honor of the birth anniversary of Dr. Sarvepalli Radhakrishnan — a legendary scholar, philosopher, teacher, and India's second President.

On this day, I find myself reflecting on the countless teachers who have shaped my life and success. When I look back, it almost feels like a miracle — the right teachers always seemed to appear at the right time. I am eternally grateful to every single one of them.


The Foundation: Family and Village Teachers

From my late grandfather, a retired headmaster, and my mother, a retired teacher, to the many teachers in my village and nearby towns, each one left an indelible mark on my journey. Growing up in a small village in India, I sometimes had to walk, bike, or take a bus to reach my teachers.

The bus rides themselves were a lesson in generosity — privately owned buses run by village families often let students travel for free. That act of kindness made education more accessible to us than I can ever put into words.

I remember vividly how some teachers, instead of biking back miles to their own villages in the scorching heat, would stay back after school and teach us at home during summer afternoons. Our summer school sessions ran from 7:00 am to 12:30 pm, and yet, their commitment never wavered.


Expanding Horizons: Varanasi and Beyond

Later, in Varanasi, I found mentors who deepened my understanding of Physics, Chemistry, and Mathematics. I also met teachers who introduced me to spoken English. As a non-native English speaker, those summer lessons after school gave me the confidence I carry even today — though, like every language, I am still learning.

My gratitude extends to all the teachers I met during my undergraduate and graduate studies, as well as those who made learning possible through online courses. The ability to access quality education from anywhere is one of the greatest gifts of our era. To all the educators creating content, sharing knowledge, and making it freely or affordably available — thank you for your selfless contribution to the world.


A Serendipitous Meeting

And today, on Teacher's Day, September 5th, 2025, serendipity reminded me once again of the power of teachers. I had the honor of meeting Professor Andrew Ng here in the Bay Area, California. I've taken his Machine Learning course on Coursera and watched countless of his lectures on YouTube. Meeting him in person was truly special.

Thank you, Professor Ng, for your extraordinary contribution to education and for inspiring millions of learners like me across the globe.


A Universal Thank You

So today, I say a big thank you — to all my teachers, to teachers everywhere, and to everyone who continues to guide, nurture, and shape the next generation.

Teaching is not just a profession; it's a calling that shapes minds, builds futures, and transforms societies. Every teacher who has ever taken the time to explain a concept, offer encouragement, or simply believe in a student's potential has contributed to making this world a better place.


The Ripple Effect

The impact of a good teacher extends far beyond the classroom. They don't just teach subjects; they teach us how to think, how to question, how to persevere, and how to dream. The lessons they impart become the foundation upon which we build our lives and careers.

As I continue my own journey of learning and growing, I carry with me the wisdom, patience, and dedication that my teachers have shown me. Their influence lives on in every problem I solve, every challenge I face, and every opportunity I get to help others learn.


With deep gratitude,
A grateful student,
Dileep


Happy Teacher's Day to all the educators around the world. Your dedication and passion for teaching continue to light the path for countless students. Thank you for everything you do.

Deep Dive into Heaps in Python

Heaps are one of the most elegant and efficient data structures in computer science, yet they're often misunderstood or overlooked. Today, we'll explore Python's heapq module and dive deep into heap algorithms, their applications, and when to use them.


What is a Heap?

A heap is a specialized tree-based data structure that satisfies the heap property:

Heap Property

In a min-heap, for any given node, the node's value is less than or equal to the values of its children.

In a max-heap, for any given node, the node's value is greater than or equal to the values of its children.

Key Characteristics

  • Complete Binary Tree: All levels are filled except possibly the last, which is filled from left to right
  • Heap Property: Parent-child relationship maintains order
  • Efficient Operations: Insert and extract in O(log n) time
  • Array Representation: Can be efficiently stored in a list/array

Python's heapq Module

Python's heapq module implements a min-heap using regular Python lists:

import heapq

# Create a heap
heap = []

# Add elements
heapq.heappush(heap, 4)
heapq.heappush(heap, 1)
heapq.heappush(heap, 3)
heapq.heappush(heap, 2)

print(heap)  # [1, 2, 3, 4]

# Extract minimum
min_val = heapq.heappop(heap)
print(min_val)  # 1
print(heap)     # [2, 4, 3]

Essential Operations

import heapq

# Initialize from existing list
numbers = [4, 1, 3, 2, 16, 9, 10, 14, 8, 7]
heapq.heapify(numbers)  # O(n) time complexity
print(numbers)  # [1, 2, 3, 4, 7, 9, 10, 14, 8, 16]

# Push and pop in one operation
new_min = heapq.heapreplace(numbers, 5)  # Pop min, then push 5
print(new_min)  # 1

# Push then pop (more efficient than separate operations)
result = heapq.heappushpop(numbers, 0)  # Push 0, then pop min
print(result)   # 0

Real-World Applications

1. Task Scheduling with Priorities

import heapq
from datetime import datetime, timedelta

class Task:
    def __init__(self, priority, name, deadline):
        self.priority = priority
        self.name = name
        self.deadline = deadline

    def __lt__(self, other):
        return self.priority < other.priority

    def __repr__(self):
        return f"Task('{self.name}', priority={self.priority})"

class TaskScheduler:
    def __init__(self):
        self.tasks = []

    def add_task(self, priority, name, deadline):
        task = Task(priority, name, deadline)
        heapq.heappush(self.tasks, task)

    def get_next_task(self):
        if self.tasks:
            return heapq.heappop(self.tasks)
        return None

    def peek_next(self):
        return self.tasks[0] if self.tasks else None

# Usage
scheduler = TaskScheduler()
scheduler.add_task(1, "Fix critical bug", datetime.now() + timedelta(hours=2))
scheduler.add_task(3, "Code review", datetime.now() + timedelta(days=1))
scheduler.add_task(2, "Update documentation", datetime.now() + timedelta(hours=6))

print(scheduler.get_next_task())  # Highest priority task

2. Finding K Largest/Smallest Elements

import heapq

def find_k_largest(nums, k):
    """Find k largest elements using a min-heap of size k"""
    if k >= len(nums):
        return sorted(nums, reverse=True)

    # Use negative values for max-heap behavior
    heap = []
    for num in nums:
        if len(heap) < k:
            heapq.heappush(heap, num)
        elif num > heap[0]:
            heapq.heapreplace(heap, num)

    return sorted(heap, reverse=True)

def find_k_smallest(nums, k):
    """Find k smallest elements efficiently"""
    return heapq.nsmallest(k, nums)

# Example usage
numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
print(find_k_largest(numbers, 3))   # [9, 6, 5]
print(find_k_smallest(numbers, 3))  # [1, 1, 2]

3. Dijkstra's Algorithm Implementation

import heapq
from collections import defaultdict

def dijkstra(graph, start):
    """Find shortest paths from start to all other vertices"""
    distances = defaultdict(lambda: float('inf'))
    distances[start] = 0
    pq = [(0, start)]
    visited = set()

    while pq:
        current_distance, current_vertex = heapq.heappop(pq)

        if current_vertex in visited:
            continue

        visited.add(current_vertex)

        for neighbor, weight in graph[current_vertex]:
            distance = current_distance + weight

            if distance < distances[neighbor]:
                distances[neighbor] = distance
                heapq.heappush(pq, (distance, neighbor))

    return dict(distances)

# Example graph
graph = {
    'A': [('B', 4), ('C', 2)],
    'B': [('C', 1), ('D', 5)],
    'C': [('D', 8), ('E', 10)],
    'D': [('E', 2)],
    'E': []
}

shortest_paths = dijkstra(graph, 'A')
print(shortest_paths)  # {'A': 0, 'C': 2, 'B': 4, 'D': 9, 'E': 11}

Performance Analysis

Operation Time Complexity Space Complexity
heappush O(log n) O(1)
heappop O(log n) O(1)
heapify O(n) O(1)
nlargest/nsmallest O(n log k) O(k)

When to Use Heaps

Good for: - Priority queues - Finding k largest/smallest elements - Streaming data scenarios - Graph algorithms (Dijkstra, Prim's) - Task scheduling

Not ideal for: - Random access to elements - Searching for arbitrary elements - When you need a max-heap (requires workarounds)


Advanced Techniques

Creating a Max-Heap

import heapq

class MaxHeap:
    def __init__(self):
        self.heap = []

    def push(self, val):
        heapq.heappush(self.heap, -val)

    def pop(self):
        return -heapq.heappop(self.heap)

    def peek(self):
        return -self.heap[0] if self.heap else None

    def __len__(self):
        return len(self.heap)

# Usage
max_heap = MaxHeap()
for val in [3, 1, 4, 1, 5]:
    max_heap.push(val)

print(max_heap.pop())  # 5
print(max_heap.pop())  # 4

Heap with Custom Objects

import heapq
from dataclasses import dataclass, field
from typing import Any

@dataclass
class PriorityItem:
    priority: int
    item: Any = field(compare=False)

    def __lt__(self, other):
        return self.priority < other.priority

# Usage
pq = []
heapq.heappush(pq, PriorityItem(2, "Second task"))
heapq.heappush(pq, PriorityItem(1, "First task"))
heapq.heappush(pq, PriorityItem(3, "Third task"))

while pq:
    priority_item = heapq.heappop(pq)
    print(f"Priority {priority_item.priority}: {priority_item.item}")

Conclusion

Heaps are incredibly versatile data structures that excel in scenarios requiring efficient priority-based operations. Python's heapq module makes them accessible and easy to use, though understanding the underlying principles helps you leverage their full power.

Key Takeaways:

  1. Use heaps for priority queues and k-largest/smallest problems
  2. Remember it's a min-heap - use negative values for max-heap behavior
  3. O(log n) operations make heaps efficient for dynamic datasets
  4. Perfect for algorithms like Dijkstra's and A* search

Next time you encounter a problem involving priorities, rankings, or the need to efficiently access extremes in a dataset, consider reaching for a heap!


Have questions about heaps or want to see more advanced applications? Let me know in the comments or reach out on Twitter!

Test-Driven Problem Solving in Coding Interviews

Coding interviews can be intimidating, but there's a systematic approach that can boost your confidence and improve your success rate: Test-Driven Development (TDD). Today, we'll explore how to apply TDD principles to coding interview problems for clearer thinking and better solutions.


Why TDD for Interviews?

Traditional interview advice often focuses on algorithms and data structures, but how you approach problems matters just as much. TDD provides:

  • Clarity of thought through concrete examples
  • Incremental progress that builds confidence
  • Automatic edge case discovery
  • Built-in verification of your solution
  • Clear communication with your interviewer

The TDD Interview Framework

1. Understand & Clarify (Red Phase)

Before writing any code, create failing tests that capture your understanding:

def test_two_sum_basic():
    # Test basic functionality
    assert two_sum([2, 7, 11, 15], 9) == [0, 1]

def test_two_sum_edge_cases():
    # Test edge cases you discover through questions
    assert two_sum([3, 3], 6) == [0, 1]  # Duplicates
    assert two_sum([1, 2], 4) == None    # No solution

Key Questions to Ask: - What should happen with duplicates? - What if no solution exists? - Are negative numbers allowed? - Can I use the same element twice?

2. Implement Incrementally (Green Phase)

Start with the simplest solution that passes your tests:

def two_sum(nums, target):
    """Find indices of two numbers that add up to target"""
    # Brute force - O(n²) but correct
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == target:
                return [i, j]
    return None

3. Optimize & Refactor (Refactor Phase)

Now improve your solution while keeping tests green:

def two_sum(nums, target):
    """Find indices of two numbers that add up to target - O(n) solution"""
    seen = {}
    for i, num in enumerate(nums):
        complement = target - num
        if complement in seen:
            return [seen[complement], i]
        seen[num] = i
    return None

Real Interview Example: Valid Parentheses

Let's walk through a complete example using the "Valid Parentheses" problem.

Step 1: Write Tests First

def test_valid_parentheses():
    # Basic cases
    assert is_valid("()") == True
    assert is_valid("()[]{}") == True
    assert is_valid("([{}])") == True

    # Invalid cases
    assert is_valid("(]") == False
    assert is_valid("([)]") == False

    # Edge cases
    assert is_valid("") == True      # Empty string
    assert is_valid("(") == False    # Unmatched opening
    assert is_valid(")") == False    # Unmatched closing
    assert is_valid("((") == False   # Multiple unmatched

Step 2: Implement Solution

def is_valid(s):
    """Check if string has valid parentheses"""
    stack = []
    mapping = {')': '(', '}': '{', ']': '['}

    for char in s:
        if char in mapping:  # Closing bracket
            if not stack or stack.pop() != mapping[char]:
                return False
        else:  # Opening bracket
            stack.append(char)

    return len(stack) == 0

Step 3: Verify and Optimize

Run all tests to ensure correctness, then consider optimizations:

def is_valid_optimized(s):
    """Optimized version with early termination"""
    if len(s) % 2 != 0:  # Odd length can't be valid
        return False

    stack = []
    pairs = {'(': ')', '[': ']', '{': '}'}

    for char in s:
        if char in pairs:  # Opening bracket
            stack.append(pairs[char])
        elif not stack or stack.pop() != char:  # Closing bracket
            return False

    return len(stack) == 0

Advanced TDD Techniques for Interviews

1. Property-Based Testing Mindset

Think about invariants and properties:

def test_binary_search_properties():
    arr = [1, 3, 5, 7, 9, 11]

    # Property: If element exists, index should be correct
    for i, val in enumerate(arr):
        assert binary_search(arr, val) == i

    # Property: If element doesn't exist, should return -1
    assert binary_search(arr, 0) == -1
    assert binary_search(arr, 12) == -1
    assert binary_search(arr, 4) == -1

2. Boundary Testing

Always test the edges:

def test_merge_intervals_boundaries():
    # Single interval
    assert merge([[1, 4]]) == [[1, 4]]

    # No overlap
    assert merge([[1, 2], [3, 4]]) == [[1, 2], [3, 4]]

    # Complete overlap
    assert merge([[1, 4], [2, 3]]) == [[1, 4]]

    # Adjacent intervals
    assert merge([[1, 3], [3, 5]]) == [[1, 5]]

    # Empty input
    assert merge([]) == []

3. Performance Testing

Consider time/space complexity in your tests:

def test_fibonacci_performance():
    import time

    # Test that optimized version is fast enough
    start = time.time()
    result = fibonacci_optimized(40)
    duration = time.time() - start

    assert result == 102334155
    assert duration < 0.001  # Should be nearly instant

Communication Strategy

During the Interview

  1. Share your test cases: "Let me start by writing some test cases to make sure I understand the problem"

  2. Explain your thinking: "This test case covers the edge case where..."

  3. Show incremental progress: "Great, my basic solution passes all tests. Now let me optimize..."

  4. Validate as you go: "Let me trace through this test case to verify my logic..."

Example Dialog

You: "Let me write a few test cases first to make sure I understand the problem correctly."

Interviewer: "Good approach, go ahead."

You: "For the two-sum problem, I'll test the basic case [2,7,11,15] with target 9, expecting indices [0,1]. I should also test duplicates like [3,3] with target 6. What should happen if there's no solution?"

Interviewer: "Return null or empty array."

You: "Perfect, I'll add that test case too. Now I'll implement a solution that passes these tests..."


Common Pitfalls to Avoid

❌ Don't Skip the Test Phase

# Bad: Jump straight to implementation
def two_sum(nums, target):
    # Hope this works...
    for i in range(len(nums)):
        # ...

✅ Start with Tests

# Good: Define expected behavior first
def test_two_sum():
    assert two_sum([2, 7, 11, 15], 9) == [0, 1]

def two_sum(nums, target):
    # Now implement to satisfy the test

❌ Don't Ignore Edge Cases

# Bad: Only test happy path
def test_basic_only():
    assert reverse_string("hello") == "olleh"

✅ Test Comprehensively

# Good: Cover edge cases
def test_reverse_string():
    assert reverse_string("hello") == "olleh"  # Basic
    assert reverse_string("") == ""            # Empty
    assert reverse_string("a") == "a"          # Single char
    assert reverse_string("ab") == "ba"        # Two chars

Practice Problems to Try

Start applying TDD to these classic problems:

  1. Two Sum - Hash map optimization
  2. Valid Parentheses - Stack-based solution
  3. Merge Intervals - Sorting and merging
  4. Binary Search - Divide and conquer
  5. Longest Substring Without Repeating Characters - Sliding window

For each problem: 1. Write comprehensive test cases first 2. Implement brute force solution 3. Optimize while keeping tests green 4. Add performance considerations


Conclusion

Test-Driven Development isn't just for production code—it's a powerful tool for coding interviews that:

  • Clarifies requirements through concrete examples
  • Reduces bugs through continuous validation
  • Demonstrates systematic thinking to interviewers
  • Builds confidence through incremental progress
  • Improves communication by making assumptions explicit

Remember: The goal isn't just to solve the problem—it's to demonstrate how you think, communicate, and approach complex challenges systematically.


Next Steps

  1. Practice TDD on LeetCode problems
  2. Time yourself writing tests vs. implementation
  3. Record yourself explaining your approach
  4. Get feedback from peers on your communication style

Ready to level up your interview game? Start with one problem today and apply the TDD approach. You'll be surprised how much clearer your thinking becomes!


Questions about TDD in interviews? Want to share your experience? Reach out on LinkedIn or Twitter!