Library / SDK
python-humanize/humanize avatar
python-humanize/humanize

humanize: turning timestamps and byte counts into readable English

Python humanize functions

765 stars206 forksPythonMIT

At a glance

What is it?
humanize is a small MIT licensed Python library that renders numbers, durations, timestamps and file sizes as phrases a person would say, in more than thirty locales. The API is a flat list of functions with a few keyword flags, and the fuzzy wording is a deliberate choice with consequences.
Who is it for?
Adopt humanize when your interface needs to say three minutes ago rather than a timestamp, especially if that interface is not English only, since the package ships gettext catalogues for more than thirty locales and switching them is a single call. Keep it out of anything where the exact number matters, because the natural language functions round and reword, and precisedelta with a minimum_unit of microseconds is documented to return milliseconds.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A flat list of functions that phrase a number

humanize is described as a modest package containing various common humanization utilities, like turning a number into a fuzzy human-readable duration such as 3 minutes ago, or into a human-readable size or throughput. That word fuzzy does the most work in that sentence, and it is the single most important thing to understand before you use the library.

The API is a flat list of module level functions, with no classes to learn and no configuration object. Integer humanization is a group of small converters:

pycon
>>> import humanize
>>> humanize.intcomma(12345)
'12,345'
>>> humanize.intword(123455913)
'123.5 million'
>>> humanize.apnumber(4)
'four'
>>> humanize.apnumber(41)
'41'

That last pair is a good example of the awkwardness the library smooths over. apnumber(4) gives four and apnumber(41) gives 41, so the function switches to words only in the range where it reads better.

The package is MIT licensed, the original author is Jason Moiron, Hugo van Kemenade is the maintainer, the default branch is main, and the project is classified as Development Status :: 5 - Production/Stable. The homepage is humanize.readthedocs.io, and the full API reference lives there rather than in the README.

There is no runtime dependency listed in the project metadata, which is worth noting for a library this small. The heavy part of what it does, gettext, comes from the standard library.

Three different ways to say how long ago something happened

The date and time functions are grouped by how precise they are, and the differences matter more than the function names suggest.

naturaldelta gives a duration with no reference point. naturalday gives a day-level phrase, and naturaltime gives a phrase anchored to now, in the past or the future. The README shows all three:

pycon
>>> humanize.naturalday(dt.datetime.now())
'today'
>>> humanize.naturaldelta(dt.timedelta(seconds=1001))
'16 minutes'
>>> humanize.naturalday(dt.datetime.now() - dt.timedelta(days=1))
'yesterday'
>>> humanize.naturalday(dt.date(2007, 6, 5))
'Jun 05'
>>> humanize.naturaldate(dt.date(2007, 6, 5))
'Jun 05 2007'
>>> humanize.naturaltime(dt.datetime.now() - dt.timedelta(seconds=3600))
'an hour ago'

The last two lines of that example show naturaldate giving a year when the date is old, and naturalday giving a bare day and month when it is recent enough. That is a relative-to-now behaviour baked into the function, which means the output of a naturalday call depends on when you make it.

For a log line, a comment timestamp or a file listing, this is exactly what you want. For a value that has to be reproducible, such as anything in a test assertion or a report that is diffed across runs, it is a problem, and the fix is either to freeze time in the test or to use precisedelta and format the date yourself.

precisedelta, and the minimum_unit flag that does not always do what you expect

precisedelta is the function to reach for when the number matters, and it has three parameters that shape the output. The base call lists the units in order:

pycon
>>> delta = dt.timedelta(seconds=3633, days=2, microseconds=123000)
>>> humanize.precisedelta(delta)
'2 days, 1 hour and 33.12 seconds'

Setting minimum_unit raises the smallest unit shown, and the example moves from hundredths of a second to whole seconds with milliseconds appended:

pycon
>>> humanize.precisedelta(delta, minimum_unit="microseconds")
'2 days, 1 hour, 33 seconds and 123 milliseconds'

The suppress parameter removes units, and format controls the numeric rendering, so you can drop days and get an hours-based total with four decimal places:

pycon
>>> humanize.precisedelta(delta, suppress=["days"], format="%0.4f")
'49 hours and 33.1230 seconds'

Now the part that will surprise you. The README has a section on smaller units, and it shows that with a four millisecond delta, the default output is a moment, while minimum_unit of milliseconds gives 4 milliseconds, and minimum_unit of microseconds also gives 4 milliseconds. Both flags produce the same string.

The same pattern shows in naturaltime, where minimum_unit of microseconds yields 4 milliseconds ago. So the microseconds level of the minimum_unit flag is accepted but does not refine the output below milliseconds. If you need microsecond precision, this is not the function for it, and the documented behaviour is the evidence.

The smaller-unit behaviour for naturaldelta is otherwise sensible. A two second delta reads as 2 seconds, while four milliseconds reads as a moment.

Sizes, throughput and three different rounding conventions

naturalsize is where the choice of unit system matters, and the function offers three ways to express the same number:

pycon
>>> humanize.naturalsize(1_000_000)
'1.0 MB'
>>> humanize.naturalsize(1_000_000, binary=True)
'976.6 KiB'
>>> humanize.naturalsize(1_000_000, gnu=True)
'976.6K'

The default is decimal megabytes, the binary flag switches to powers of 1024 with the IEC unit names, and the gnu flag produces the short form without a space that GNU tools use. The difference between 1.0 MB and 976.6 KiB for the same byte count is the difference between a marketing number and an honest one, and picking the wrong convention is a common source of confusion in file managers.

The formatting style elsewhere follows the same idea. naturalsize always keeps one decimal place, which is why the example does not produce a bare 1 MB.

For values that are not sizes, intcomma inserts thousands separators and intword switches to a short scale word above a million, giving 123.5 million for 123455913 and 12.3 billion for 12345591313. The threshold behaviour is the useful part: below the cutoff you get an exact number with separators, above it you get a rounded magnitude.

That rounding is where the caveat belongs. intword and apnumber are lossy, and a value that has to be exact should not go through them.

Fractions and scientific notation for numbers that do not read well

fractional turns a float into a fraction string, which is the shortest way to show a repeating value without a decimal point:

pycon
>>> humanize.fractional(1/3)
'1/3'
>>> humanize.fractional(1.5)
'1 1/2'
>>> humanize.fractional(0.3)
'3/10'
>>> humanize.fractional(0.333)
'333/1000'
>>> humanize.fractional(1)
'1'

Look at what the 0.333 case does. It does not approximate to a third, it prints 333/1000, which is the exact rational value of the binary float. And 0.3 becomes 3/10 rather than 0.3, so the function finds a small rational approximation instead of exposing the float's true denominator. The trade-off is that the output is not obviously the number you passed in, and a test asserting on 0.3 would see 3/10.

scientific is the other numeric helper, and it produces the form with a multiplication sign and a superscript exponent:

pycon
>>> humanize.scientific(0.3)
'3.00 x 10⁻¹'
>>> humanize.scientific(500)
'5.00 x 10²'
>>> humanize.scientific(1**10, precision=1)
'1.0 x 10⁰'
>>> humanize.scientific(1**10, precision=0)
'1 x 10⁰'

The precision argument controls the mantissa, and the examples show it stepping down from two decimals to one to none. A string input is accepted too, since the example passes "20000" and gets 2.00 x 10⁴.

Localization is gettext, and the locale is global state

The package is localized to more than thirty locales, listed in the README, including Arabic, Bengali, Hebrew, Japanese, Klingon, Persian, Simplified Chinese, Ukrainian and Uzbek, among many more. The mechanism is gettext, and the interface for switching is two calls:

pycon
>>> humanize.naturaltime(dt.timedelta(seconds=3))
'3 seconds ago'
>>> _t = humanize.i18n.activate("ru_RU")
>>> humanize.naturaltime(dt.timedelta(seconds=3))
'3 секунды назад'
>>> humanize.i18n.deactivate()
>>> humanize.naturaltime(dt.timedelta(seconds=3))
'3 seconds ago'

Note the shape of that. activate returns a translation object, which the example assigns to a throwaway variable, and the locale change affects every subsequent call in the process until deactivate is called. This is process wide mutable state, not a per call option, so it does not compose well with a web application that serves requests for users in different languages, unless you activate and deactivate around each request.

There is also an error path worth knowing. Activating a locale with no catalogue raises an error, and you can point the lookup at your own directory:

pycon
>>> humanize.i18n.activate("xx_XX")
FileNotFoundError: [Errno 2] No translation file found for domain: 'humanize'
>>> humanize.i18n.activate("pt_BR", path="path/to/my/own/translation/")
<gettext.GNUTranslations instance ...>

That path argument is also how you supply translations the project does not ship.

Adding phrases and locales, and the state of the project

Translating the project yourself is a two step workflow. The first script extracts new phrases from the source into humanize.pot, merges them into each locale's .po file and compiles the binary .mo catalogues:

sh
scripts/update-translations.sh

The second step creates a brand new locale from the template:

sh
msginit -i humanize.pot -o humanize/locale/<locale name>/LC_MESSAGES/humanize.po --locale <locale name>

where the locale name is an abbreviation such as en_GB, pt_BR or a bare ru or fr. The README asks that a new language be listed at the top of the README, which is a manual step and tells you how much of the process depends on a person noticing.

The build configuration is where the compiled catalogues come from. The build backend is hatchling with hatch-vcs deriving the version from the repository, the version is dynamic, and the build artifacts list includes *.mo, so the compiled translation files ship inside the wheel. The layout is a src/ directory with a tests/ directory, tox.ini, a .pre-commit-config.yaml, a requirements-mypy.txt, a mkdocs.yml for the documentation and a scripts/ directory holding the translation tooling.

The maintenance record is healthy. Version 4.14.0 was released on 2025-10-15, 4.15.0 on 2025-12-20 and 4.16.0 on 2026-06-30, and the last push was on 2026-10-06, so the 4.x line has been moving through the year. The development extras include pytest, pytest-cov, freezegun, pytest-benchmark and pytest-codspeed, and freezegun in particular is how a library with time-dependent output keeps its own tests stable.

Editorial conclusion

Adopt humanize when your interface needs to say three minutes ago rather than a timestamp, especially if that interface is not English only, since the package ships gettext catalogues for more than thirty locales and switching them is a single call. Keep it out of anything where the exact number matters, because the natural language functions round and reword, and precisedelta with a minimum_unit of microseconds is documented to return milliseconds. Before you rely on it, note that the package requires Python 3.11 or newer, that the last push was on 2026-10-06 with version 4.16.0 released on 2026-06-30, and that the locale you activate is process wide state you must deactivate yourself.

Frequently asked questions

How do I install humanize and format a timestamp as readable text?

Install it with python3 -m pip install --upgrade humanize, or clone the repository and run python3 -m pip install -e . from the source tree. Then call humanize.naturaltime for a relative phrase, humanize.naturalday for a day level phrase, or humanize.naturaldelta for a duration with no reference point.

How do I switch humanize to another language at runtime?

Call humanize.i18n.activate with a locale such as ru_RU, and humanize.i18n.deactivate to go back to the default. The active locale is process wide state, so it affects every later call until you deactivate it, and you can pass a path argument to load translations from your own directory.

Does humanize support microsecond precision in its output?

The documented behaviour is that a minimum_unit of microseconds still returns milliseconds. A four millisecond delta with minimum_unit set to microseconds is shown as 4 milliseconds, not 4.000 milliseconds, and precisedelta with the same flag reports 123 milliseconds rather than microseconds.

How does humanize choose between decimal and binary file sizes?

naturalsize uses decimal megabytes by default, the binary flag switches to 1024 based units with names such as KiB, and the gnu flag produces the short form without a space, for example 976.6K for a million bytes.

Official sources

  1. License: MIT
  2. Project website
  3. python-humanize/humanize on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/python-humanize-humanize.svg)](https://hysenlabs.com/projects/python-humanize-humanize)

Was this useful?