camelCase to snake_case in Python

re.sub(r"([a-z\d])([A-Z])", r"\1_\2", re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)).lower()

That one line turns HTTPServer into http_server and getHTTP2Client into get_http2_client. It makes two passes. The inner re.sub splits a run of capitals before its last capital when a lowercase letter follows, so HTTPServer becomes HTTP_Server. The outer one puts an underscore between a lowercase letter or digit and the capital after it. Then everything is lowercased.

import re

def to_snake(name: str) -> str:
    name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
    name = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", name)
    return name.lower()

for name in ["HTTPServer", "getHTTPResponseCode", "userID", "XMLHttpRequest", "base64Encode", "getHTTP2Client"]:
    print(f"{name:20} {to_snake(name)}")
HTTPServer           http_server
getHTTPResponseCode  get_http_response_code
userID               user_id
XMLHttpRequest       xml_http_request
base64Encode         base64_encode
getHTTP2Client       get_http2_client

The tool below converts the same six names, one per line, and its snake_case row matches.

Why one regex isn’t enough

The version most people write first puts an underscore before every capital:

re.sub(r"(?<!^)(?=[A-Z])", "_", "HTTPServer").lower()  # 'h_t_t_p_server'
re.sub(r"([A-Z])", r"_\1", "HTTPServer").lower()        # '_h_t_t_p_server'
re.sub(r"(?<!^)(?=[A-Z])", "_", "userID").lower()      # 'user_i_d'

Acronyms fall apart, and without the lookbehind a leading capital adds a leading underscore. Splitting only where a lowercase letter meets a capital, ([a-z])([A-Z]), goes wrong the other way: HTTPServer becomes httpserver. The first pattern of the pair handles the capital run, the second handles ordinary word breaks.

Digits

([a-z\d]) lets a digit end a word, so base64Encode keeps base64 together. A digit never starts a word here: parseHTTPResponse2xx gives parse_http_response2xx, while the tool below treats 2xx as a word of its own and writes parse_http_response_2xx. Choose one rule and use it everywhere, or names won’t match across systems.

inflection and non-ASCII letters

The inflection package, a port of the Rails inflector, uses the same two patterns and also turns - into _:

import inflection

inflection.underscore("HTTPServer")      # 'http_server'
inflection.underscore("getHTTP2Client")  # 'get_http2_client'

[A-Z] and [a-z] match ASCII only, so caféBar comes out as cafébar. The standard re module has no Unicode letter classes, but the third-party regex module does: written as (\p{Lu}+)(\p{Lu}\p{Ll}) and ([\p{Ll}\d])(\p{Lu}), the same two patterns turn caféBar into café_bar.

The reverse: snake_case to camelCase

def to_camel(name: str) -> str:
    first, *rest = name.split("_")
    return first + "".join(word.capitalize() for word in rest)

to_camel("get_http2_client")              # 'getHttp2Client'
inflection.camelize("http_server", False)  # 'httpServer'

The round trip loses acronyms: HTTPServer goes to http_server and comes back as httpServer. If an API needs the original names, keep a mapping instead of converting back.

Try it in the Case Converter

16 words: HTTP · Server · get · HTTP · Response · Code · user · ID · XML · Http · Request · base64 … and 4 more

  • camelCasewords run together, each after the first capitalized
    httpServer getHttpResponseCode userId xmlHttpRequest base64Encode getHttp2Client
  • PascalCaseevery word capitalized, run together
    HttpServer GetHttpResponseCode UserId XmlHttpRequest Base64Encode GetHttp2Client
  • snake_caselower case, joined with underscores
    http_server get_http_response_code user_id xml_http_request base64_encode get_http2_client
  • kebab-caselower case, joined with hyphens
    http-server get-http-response-code user-id xml-http-request base64-encode get-http2-client
  • CONSTANT_CASEupper case, joined with underscores
    HTTP_SERVER GET_HTTP_RESPONSE_CODE USER_ID XML_HTTP_REQUEST BASE64_ENCODE GET_HTTP2_CLIENT
  • Title Casea, an, and, of, the, to, in, on, for and or stay lower unless first or last
    Http Server Get Http Response Code User Id Xml Http Request Base64 Encode Get Http2 Client
  • Sentence casethe first word capitalized, the rest lower
    Http server Get http response code User id Xml http request Base64 encode Get http2 client
  • lower caseevery letter lower
    http server get http response code user id xml http request base64 encode get http2 client
  • UPPER CASEevery letter upper
    HTTP SERVER GET HTTP RESPONSE CODE USER ID XML HTTP REQUEST BASE64 ENCODE GET HTTP2 CLIENT
  • URL sluglower-case ASCII, accents folded, single hyphens
    http-server-get-http-response-code-user-id-xml-http-request-base64-encode-get-http2-client

Runs in your browser. Nothing you enter is sent anywhere.