The Cipher Wheel
An easy Python interview practice problem on DataDriven. Write the Python and run it against test cases, with instant feedback.
- Domain
- Python
- Difficulty
- easy
- Seniority
- L3
- Asked in
- TetraScience
Problem
Your team's data masking tool swaps characters according to a per-tenant config. Given a plaintext string text and a mapping from characters to their replacements, return the masked string, leaving any character that has no entry in mapping untouched.
Every letter has an alias - you just need the right codebook.
Example 1
Input:
text = "hello", mapping = {"e":"a","h":"j","l":"m","o":"p"}Output:
"jammp"
Example 2
Input:
text = "abc", mapping = {"a":"x"}Output:
"xbc"
Worked solution and explanation
What this really is
Strip the cipher costume and this is a single-pass character substitution with a fallback rule: replace what is in the codebook, pass through what is not. The whole problem lives in that 'is not' branch. The trap candidates fall into is reaching for if ch in mapping with a separate else branch, which works but reads like twice the code and invites a subtle bug the day someone edits one branch and forgets the other. The clean move is mapping.get(ch, ch): look up the character, and default to the character itself when it is absent. Land that one idiom and the rest is plumbing.
Break down the requirements
Step 1: Iterate through each character
Walk the input one character at a time. Order matters: the output mirrors the input position for position, so there is nothing to sort or group.
Step 2: Look up each character with a self-default
mapping.get(ch, ch) does the entire decision in one expression: return the replacement if the key exists, otherwise return the original character. This is where the unmapped-character requirement gets satisfied for free.
Step 3: Join the pieces into one string
Feed the substituted characters straight into ''.join(...) so the new string is built in one allocation rather than rebuilt on every step.
The solution
def decode_string_mapping(text: str, mapping: dict) -> str:
return "".join(mapping.get(ch, ch) for ch in text)- Concatenate in a loop
- result = ""
for ch in text:
result += mapping.get(ch, ch)
return result
Strings are immutable, so
+=can allocate a fresh string each iteration: O(n^2) in the worst case. - Join a generator
- return "".join(mapping.get(ch, ch) for ch in text)
joinsizes the buffer once and fills it: a clean O(n) pass and the form a reviewer expects to see.
Common follow-up questions
- What if the substitution should be applied case-insensitively? (Tests normalizing the key before lookup while still emitting original-case characters for the unmapped ones.)
- How would you reverse the masking to recover the original text? (Tests inverting the dict and reasoning about collisions when two keys share a replacement value.)
- What changes if the input is a multi-gigabyte file instead of a string? (Tests a streaming approach: substitute chunk by chunk instead of materializing the whole input.)