This solved function splits a log line into a level and a message. The message is allowed to contain colons, which is the entire reason it works.
def split_log_line(line):
level, message = line.split(":", 1)
return level.strip(), message.strip()
# split_log_line('ERROR: disk full: /dev/sda1')
# -> ('ERROR', 'disk full: /dev/sda1')
# without the 1, this raises: too many values to unpack
- maxsplit stops after the first separator
split(':', 1) makes at most one cut, so you always get exactly two pieces and everything after the first colon stays together in the second. Without it, a message containing a colon produces three pieces and the unpacking raises.
- Unpacking is a promise about the shape
level, message = ... says out loud that there are exactly two parts. When that promise can be broken by user data, either guarantee it with maxsplit or check the shape first — never leave it to chance.
- Strip each piece, not the whole string
The spaces you care about are around each part, and they arrive at different times: ' ERROR : hi ' has them in three places. Stripping the pieces after splitting handles all of them without any of the cases having to be enumerated.
Two levels of splitting: the whole string into segments, then each segment into a key and a value. Decide what should happen to an empty segment before you write the second split.
text.split(';') gives the segments. Strip each one; if it is empty, skip it with continue. If it has no '=' at all, that is the case the contract says to raise on.
Build a dict. For each stripped, non-empty segment: check '=' is present, then key, value = segment.split('=', 1) — the 1 is what keeps an = inside the value intact — and store key.strip() against value.strip(). Assigning into the dict gives you last-wins for free.
def parse_pairs(text): result = {} for segment in text.split('____'): segment = segment.strip() if not segment: ____ if '=' not in segment: raise ____(...) key, value = segment.split('=', ____) result[key.strip()] = value.strip() return result
def parse_pairs(text):
result = {}
for segment in text.split(";"):
segment = segment.strip()
if not segment:
continue
if "=" not in segment:
raise ValueError(f"segment has no '=': {segment!r}")
key, value = segment.split("=", 1)
result[key.strip()] = value.strip()
return result
An empty segment is skipped; a segment without an equals sign raises. Both are 'not a valid pair', so the difference is not obvious from the data — it comes from what each one implies. Trailing semicolons are produced routinely by correct code, whereas 'oops' with no equals means something upstream is broken and someone should hear about it. Whenever you write a parser, decide this deliberately and put it in the docstring, because callers cannot guess.
f'{segment!r}' formats with repr instead of str, so the message shows quotes and any invisible characters. When the bad segment turns out to be ' ' or 'a\u00a0=1' with a non-breaking space, repr is the difference between a five-second diagnosis and an afternoon.
result[key] = value overwrites, so duplicates resolve without any code. That is the right default for configuration. It is the wrong default for HTTP query strings, where ?tag=a&tag=b legitimately means two tags — which is why those parsers return a list per key instead. The data structure encodes the policy.
Do not hand-roll this for formats that already have a parser. Cookies, query strings, ini files and .env files each have edge cases around quoting, escaping and encoding that this function ignores entirely — urllib.parse.parse_qs, http.cookies and configparser exist and are correct. Write your own only for a small internal format you control, and the moment somebody asks to put a semicolon inside a value, stop and pick a real format instead.
- Support quoted values so a="1;2" keeps its semicolon, and notice how much harder splitting becomes once a separator can be escaped.
- Add an optional coerce flag that converts values that look like integers or booleans, then write down why that is dangerous for a value like '007'.
- Return a list of values per key instead of last-wins, and decide what the return type should be so callers are not surprised.