Walk all left rows, looking up each key in a right-table index and filling None when no match exists. With pandas, pd.merge(..., how='left') keeps every left row and fills missing right-side values with NaN. The library half converts NaN to None so the RESULT is comparable to the naive half.

By hand

With pandas

pd.merge(left, right, on='id', how='left') preserves all left rows. The unmatched right value becomes NaN, which forces the score column from int64 to float64 (visible in score dtype: and score raw:). The library then normalizes NaN → None for a RESULT comparable to the naive half.

naive.py
left = [
    {'id': 1, 'name': 'al'},
    {'id': 2, 'name': 'bo'},
    {'id': 3, 'name': 'cy'},
]
right = [
    {'id': 1, 'score': 80},
    {'id': 3, 'score': 70},
]
right_idx = {}
for r in right:
    right_idx[r['id']] = r['score']
joined = []
for l in left:
    score = right_idx.get(l['id'])
    joined.append((l['name'], score))
print('RESULT:', joined)
library.py
import pandas as pd
import math
from dalib.display import set_display
set_display()

left = pd.DataFrame({'id': [1, 2, 3], 'name': ['al', 'bo', 'cy']})
right = pd.DataFrame({'id': [1, 3], 'score': [80, 70]})
merged = pd.merge(left, right, on='id', how='left')
scores = [int(s) if not math.isnan(s) else None for s in merged['score'].tolist()]
print('columns:', merged.columns.tolist())
print('score dtype:', merged['score'].dtype)
print('score raw:', merged['score'].tolist())
print('ids:', merged['id'].tolist())
print('names:', merged['name'].tolist())
print('scores:', scores)
print('RESULT:', list(zip(merged['name'].tolist(), scores)))
columns: ['id', 'name', 'score']
score dtype: float64
score raw: [80.0, nan, 70.0]
ids: [1, 2, 3]
names: ['al', 'bo', 'cy']
scores: [80, None, 70]
RESULT: [('al', 80), ('bo', None), ('cy', 70)]

Implementation notes

  • NaN vs None: pandas fills missing join values with NaN (a float), not Python None. The score column's dtype becomes float64 because integer columns cannot hold NaN natively. The library half uses int(s) if not math.isnan(s) else None to convert to Python-native types and restore comparability with the naive RESULT. (Pandas ≥1.0 Int64 nullable integer type can hold pd.NA, but converting that is a separate pattern.)
  • A left join is the most common join in practice: it keeps the "primary" table whole and attaches optional enrichment from the right, making missing values explicit rather than silently dropping rows.
  • how='right' keeps all right rows; how='outer' keeps all rows from both sides, filling NaN on whichever side lacks a match.
  • Cross-reference: left-join-with-missing (python-data-basics) for the pure-Python .get()-with-None version.