Think in layers.
BFS starts at the source and expands outward one edge at a time. The queue is not just a data structure here. It is the mechanism that preserves the layer order.
If a node is discovered while processing layer k, then it belongs to layer k + 1. Because the queue processes earlier discoveries first, no deeper route gets to cut the line.
The invariant.
Before BFS starts processing any node at distance k + 1, it has already processed every reachable node at distance k.
Base case: the source is distance 0. Step: every neighbor discovered from a distance-k node is distance k + 1, and FIFO order keeps those nodes behind the rest of the current layer.
Code as movement.
from collections import deque
def bfs(graph, source):
distance = {source: 0}
queue = deque([source])
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if neighbor in distance:
continue
distance[neighbor] = distance[node] + 1
queue.append(neighbor)
return distance
The common mistake.
Mark a node when you enqueue it, not when you pop it. If you wait until pop time, the same node can enter the queue multiple times from siblings in the same layer.