Two ways to raise the first letter of a word. They differ on every piece that already had a capital in it.
'id'.capitalize() # -> 'Id' 'ID'.capitalize() # -> 'Id' the rest was lowercased too 'ID'[:1].upper() + 'ID'[1:] # -> 'ID' # And on an empty piece, which a double underscore produces ''.capitalize() # -> '' fine ''[0].upper() # IndexError ''[:1].upper() + ''[1:] # -> '' slices do not mind
- capitalize() is title-casing one word, not raising one letter
It uppercases the first character and lowercases every other one, which is right for prose and wrong for identifiers — user_ID becomes userId, http_URL becomes httpUrl, and the acronyms your API deliberately capitalised are gone. When you only mean 'change this one character', say so.
- Slice, do not index, when the piece might be empty
piece[0] raises IndexError on an empty string; piece[:1] returns an empty string and carries on. Since a run of underscores produces empty pieces, the slice version needs no length check at all. Slicing’s tolerance of out-of-range bounds is exactly what you want when a piece might legitimately be nothing.
- Leading underscores are meaning, not formatting
A single leading underscore says 'internal' and a double says 'name mangle this' — stripping them changes what the name means to Python and to every reader. Peeling them off, converting the rest, and putting them back is three lines and keeps the convention intact.
Take the leading underscores off first and put them back at the end. What is left is a plain split-and-join, with a rule that applies to every piece except the first.
name.lstrip('_') removes the leading run; the count you removed is the difference in length. Split the rest on '_' and drop the empty pieces that doubled and trailing underscores leave behind.
Do not use capitalize() — it lowercases the rest of the piece. piece[:1].upper() + piece[1:] changes exactly one character and cannot raise on an empty piece. The first piece is joined in as it is.
def snake_to_camel(name): stripped = name.lstrip('_') leading = '_' * (len(name) - len(____)) pieces = [piece for piece in stripped.split('_') if ____] if not pieces: return ____ head = pieces[0] tail = [piece[:1].____() + piece[1:] for piece in pieces[1:]] return leading + head + ''.____(tail)
def snake_to_camel(name):
stripped = name.lstrip("_")
leading = "_" * (len(name) - len(stripped))
pieces = [piece for piece in stripped.split("_") if piece]
if not pieces:
return name
head = pieces[0]
# Not capitalize(): it would lowercase the rest of the piece and
# turn user_ID into userId.
tail = [piece[:1].upper() + piece[1:] for piece in pieces[1:]]
return leading + head + "".join(tail)
lstrip removes the underscores and does not say how many, so the length difference is what reconstructs them. It is a small trick worth recognising: whenever a string method throws away information you need, the lengths before and after usually still hold it.
`if piece` drops what a double underscore leaves in the middle and what a trailing underscore leaves at the end — two contract lines satisfied by one condition, with no separate cases. Dropping the empties before choosing the head also means pieces[0] is a real piece.
After stripping and filtering there is nothing left, and the leading underscores alone are the whole name — so returning `name` is both correct and the shortest way to say it. Returning `leading` would be equivalent today and would quietly stop being so if the stripping rule ever changed.
If you are converting whole payloads rather than the odd name, the serialisation layer should do it: Pydantic’s alias_generator, marshmallow’s data_key, or your ORM’s naming convention — one declaration instead of a call at every boundary. And be careful about round-tripping: camel_to_snake and snake_to_camel are not inverses for names containing acronyms or digits, so a system that converts in both directions needs to store one canonical form rather than convert on demand.
- Add a pascal flag that capitalises the first piece too, and decide what it should do to a leading underscore.
- Check the round trip against camel_to_snake for every case here and find the ones that do not survive it.
- Handle a name that is already camelCase but contains an underscore — 'userName_id' — and decide what the right answer even is.