This solved function picks between seconds and minutes. Yours picks between five units, which is the same idea in a loop rather than an if.
def duration(seconds):
if seconds < 60:
return f'{seconds} s'
return f'{seconds / 60:.1f} min'
# duration(45) -> '45 s'
# duration(150) -> '2.5 min'
- The format spec does the rounding
f'{value:.1f}' renders one digit after the point and rounds to get there — no round() call, no str() gymnastics. Everything after the colon inside an f-string is a formatting instruction, and :.1f is the one you will reach for most.
- The small case leaves early
Handling 'it is already small enough' first, with its own return, means the rest of the function only has to deal with the interesting case. That is why there is no else here and why yours will not need one either.
- Whole numbers and decimals are formatted differently on purpose
'45 s' uses the integer directly while '2.5 min' asks for a decimal. Showing '45.0 s' would be noise. Deciding which values deserve decimals is a design choice, not a rounding accident.
Handle the two easy things first: refuse a negative count, and return straight away when the number is already under 1024. Everything left over needs at least one division.
Walk through the units above B one at a time. Divide by 1024, then ask whether the number is now small enough to show. If it is, format it with f'{size:.1f} {unit}' and return.
Guard the negative. Return f'{count} B' when count < 1024. Otherwise set size = float(count) and loop over ('KB', 'MB', 'GB', 'TB'): divide size by 1024 each time, and return the formatted string as soon as size is under 1024 — or when you have reached the last unit and have nowhere else to go.
def human_bytes(count): if count < 0: raise ValueError(...) if count < 1024: return f'{count} B' size = float(count) for unit in ('KB', 'MB', 'GB', 'TB'): size /= ____ if size < ____ or unit == '____': return f'{size:.1f} {unit}'
def human_bytes(count):
if count < 0:
raise ValueError(f"count cannot be negative, got {count}")
if count < 1024:
return f"{count} B"
size = float(count)
for unit in ("KB", "MB", "GB", "TB"):
size /= 1024
if size < 1024 or unit == "TB":
return f"{size:.1f} {unit}"
Without `or unit == 'TB'` a big enough number divides past every unit, the loop ends, and the function falls off the bottom returning None. Any loop that is expected to return from inside itself needs an answer to 'what if it never does' — either a final return or a condition that guarantees the last pass fires.
Not strictly required — / always produces a float in Python 3 — but writing it down says 'this is now a measurement, not a count'. The distinction is why bytes print as 512 and kilobytes print as 1.0.
Disk vendors sell base-1000 gigabytes; operating systems mostly show base-1024 ones, which is why a '500 GB' drive shows up as 465 GB. Neither is wrong, but silently mixing them is. If this went in a real codebase the constant would be named — KIB = 1024 — so the choice is visible to the next reader.
Do not build this from scratch in production if a dependency is acceptable — there are well-tested libraries that also handle localisation, and '1,5 MB' with a comma is correct in most of Europe. Write it yourself when you want no dependency or exact control of the format; reach for a library the moment users in different locales are involved.
- Add a `binary=False` option that switches to base-1000 with the units kB, MB, GB — and decide what the parameter should be called so the caller cannot misread it.
- Trim the pointless decimal so 1024 renders as '1 KB' but 1536 still renders as '1.5 KB'. Getting this right without special cases is harder than it looks.
- Extend past TB to PB and EB, then work out at what point a Python int stops being exactly representable as a float and whether that matters here.