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

Dict lookup with a self-default, joined once
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) join sizes the buffer once and fills it: a clean O(n) pass and the form a reviewer expects to see.

Common follow-up questions