Repetition and negative slicing do all the work. Read the last line carefully — it is the same trap as taking the last zero lines of a file.
print('*' * 5) # -> '*****' repetition builds the mask
print('*' * 0) # -> '' zero repetitions is empty, not an error
secret = 'abcdef'
print(secret[-4:]) # -> 'cdef' the last four
print(secret[-0:]) # -> 'abcdef' the whole thing - there is no negative zero
- Multiplying a string repeats it
'*' * n gives exactly n asterisks, and n of zero gives an empty string rather than an error. That makes building a mask of the right length one expression, with no loop and no special case for a short value.
- value[-0:] is value[0:], which is everything
Zero has no sign, so the minus disappears and the slice starts at the beginning. In a masking function that means asking to reveal *nothing* reveals *everything* — the worst possible direction for a bug like this to fail in.
- Length is information too
Keeping the result the same length as the input tells a user their key is the long one rather than the short one, which is often exactly the recognition they need. A fixed-width mask would hide that, and sometimes that is the right call instead.
The answer is a run of asterisks joined to the end of the value. Work out how many asterisks, then which part of the value survives — and check what your slice does when the count is zero.
The mask is '*' * (len(value) - visible) and the tail is value[-visible:]. That works for every case except one, and the worked example says which.
Guard the negative. Then handle 'mask everything' — when visible is zero or the value is no longer than visible — by returning asterisks the length of the value. Otherwise join the mask to the tail.
def mask_secret(value, visible): if visible < 0: raise ValueError(...) if visible ____ 0 or len(value) <= visible: return '____' * len(value) return '*' * (len(value) - ____) + value[____:]
def mask_secret(value, visible):
if visible < 0:
raise ValueError(f"visible cannot be negative, got {visible}")
if visible == 0 or len(value) <= visible:
return "*" * len(value)
return "*" * (len(value) - visible) + value[-visible:]
`visible == 0` exists because of the negative-zero slice; `len(value) <= visible` exists because a four-character secret shown four characters at a time is not masked at all. They are different bugs with the same fix, so they share a branch — but knowing they are two reasons matters when somebody later tries to simplify it.
A negative count would return value[1:] — the secret with one character missing, and no asterisks at all. Most off-by-one bugs produce something obviously wrong; this one produces something that looks like a working function and leaks. When the wrong answer is dangerous rather than merely incorrect, guard it explicitly.
The full value is still in memory, still in the log line that printed it, and still in whatever variable it came from. This function stops a human reading a screen; it does nothing about a database dump. Real secret handling means not having the value in the first place — tokens with short lifetimes, and a vault that only ever hands out the last four.
Do not mask and then log the result as though the problem is solved — if the secret reached your logging call, it reached your process, and the fix is upstream. Do not invent your own masking for card numbers either: PCI rules are specific about how many digits may be shown and where, and getting it wrong is a compliance problem rather than a style one. And if the masked value is going into a diff or a test fixture, a fixed-width mask is better, because a variable-length one leaks how long each secret was.
- Add a mask character parameter so a caller can use a bullet instead of an asterisk, and check what happens if they pass a two-character string.
- Show the first few characters as well — sk_live_****1234 — which is what most key displays actually do, and decide how short a value has to be before that stops being safe.
- Make the mask a fixed width regardless of the value’s length, and write down which threat each of the two designs defends against.