Skip to main content

nepkit

                      _     _  _
 _ __    ___   _ __  | | __(_)| |_
| '_ \  / _ \ | '_ \ | |/ /| || __|
| | | ||  __/ | |_) ||   < | || |_
|_| |_| \___| | .__/ |_|\_\|_| \__|
              |_|

Typed Bikram Sambat ↔ Gregorian date conversion for Python, as a library and a command-line tool.

CI PyPI Python License

Status: published, pre-1.0. The library and CLI both work and are tested, but the API and the CLI's output shapes may still change before 1.0. Pin a version if you script against stdout, or use --json instead. See STATUS.md for what's changed release to release.

DEMO.md walks through every command, option, and failure mode with real captured output.

Why

Bikram Sambat is Nepal's official calendar, and converting to and from it is not arithmetic. Gregorian leap years follow a rule you can write down; BS month lengths do not. They vary between 29 and 32 days with no generating formula, are fixed by observation, and are published by Nepal's Panchanga authority. Every correct converter is therefore a lookup table plus one verified anchor date — which means the data matters more than the code, and most of the work in this repo went into the data.

The existing Python options are small, mostly unmaintained packages with undocumented year ranges, no type hints, and no statement of where their numbers came from or how far they can be trusted. nepkit aims to be one typed, tested converter that is explicit about all three.

Install

Requires Python 3.12+.

pip install nepkit       # library and CLI
uv add nepkit            # into a uv project
uv tool install nepkit   # the CLI on its own, isolated

Typer is the only direct dependency, plus pyreadline3 on Windows alone, which gives the interactive prompt the line editing CPython omits there. The calendar table is bundled, so nepkit never touches the network.

Windows (Git Bash)

uv tool install puts nepkit.exe in uv's executable directory, which Git Bash does not carry on PATH by default — the install reports success and the command is still not found. Add the directory, then confirm:

uv tool install nepkit
uv tool dir --bin        # C:\Users\you\.local\bin
uv tool update-shell     # add it to the Windows user PATH

uv tool update-shell writes to the HKCU\Environment registry key rather than to ~/.bashrc, so only a newly launched tab sees it, and Windows does not reliably notify running processes of the change (uv#17331). If a fresh tab still comes up empty, set it from bash instead — under Git Bash $HOME is /c/Users/you, so this resolves to the same directory uv tool dir --bin prints:

echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc

Tab completion is the same one-liner as on Linux:

nepkit --install-completion

It appends a source line to ~/.bashrc pointing at ~/.bash_completions/nepkit.sh, spelled as a Windows path (C:\Users\...). That looks wrong inside a bash file, but MSYS2 resolves both spellings, so it works as written.

Library

from datetime import date

from nepkit import BSDate, __version__, ad_to_bs, bs_to_ad

bs_to_ad(BSDate(2081, 4, 15))  # date(2024, 7, 30)
ad_to_bs(date(2024, 7, 30))  # BSDate(year=2081, month=4, day=15)
__version__  # "0.4.0"

BSDate validates on construction, so if you are holding one it is a real date in the supported range:

from nepkit import BSDate, days_in_month

days_in_month(2081, 4)  # 32
days_in_month(2081, 9)  # 29

BSDate(2081, 4, 33)  # raises InvalidDateError
BSDate(2081, 13, 1)  # raises InvalidDateError
BSDate(2095, 1, 1)  # raises DateOutOfRangeError

ad_to_bs takes a datetime.date, so a malformed Gregorian date is impossible by construction — Python's own constructor rejects it before nepkit is involved.

BSDate mirrors datetime.date's string handling, and parse_bs_date/ format_bs_date (with Gregorian counterparts parse_ad_date/format_ad_date) cover the human-readable forms the CLI accepts and prints — the CLI is a thin layer over these, not a separate implementation:

from nepkit import BSDate, format_bs_date, parse_ad_date, parse_bs_date

bs = BSDate(2081, 4, 15)
str(bs)  # "2081-04-15"
BSDate.fromisoformat("2081-04-15")  # BSDate(year=2081, month=4, day=15)

# "D Month YYYY", the month matched case-insensitively against a common
# romanisation variant or Devnagari, not just BS_MONTH_NAMES's one spelling
parse_bs_date("15 Baishakh 2081")  # BSDate(year=2081, month=1, day=15)

format_bs_date(bs, named=True)  # "15 Shrawan 2081"
format_bs_date(bs, named=True, devnagari=True)  # "१५ साउन २०८१"

parse_ad_date("30 Jul 2024")  # date(2024, 7, 30)

BSDate.isoformat()'s result is not an ISO 8601 date — datetime.date.fromisoformat will accept it and silently return a different, wrong Gregorian day. It is only ever meaningful alongside something that says it is Bikram Sambat, the same way nepkit's CLI always prints a BS/AD label alongside it.

Errors

NepkitError
├── CalendarDataError   the bundled table is malformed; raised at import
└── DateError
    ├── InvalidDateError      not a real BS date (month 13, day 33, ...)
    └── DateOutOfRangeError   a real date, but outside the bundled range

The split between the last two is the one that earns its keep. BS 2095-03-12 is a perfectly real date that nepkit simply has no data for, and a caller can reasonably catch that and fall back or report the supported range. BS 2081-13-01 is not a date at all, and catching it is always a mistake. Catch DateError if you only need "the user gave me something I can't convert".

CLI

pip install and uv tool install put a nepkit command on your PATH. uv add installs into the project's .venv without activating it, so reach the command with uv run nepkit — or activate the venv first.

$ nepkit --version
nepkit 0.4.0

$ nepkit bs2ad 2081-04-15
AD 30 Jul 2024 (2024-07-30) Tue

$ nepkit ad2bs "30 July 2024"
BS 15 Shrawan 2081 (2081-04-15) Tue

$ nepkit today
BS २७ साउन २०८३ १४:३२ बुध
AD 12 Aug 2026 14:32 Wed

$ nepkit range
BS 2000-01-01 .. 2090-12-30  (years 2000-2090)
AD 1943-04-14 .. 2034-04-13

Direction is always explicit, and has to be: the BS and AD year numbers overlap from 2000 to 2034, so 2024 is a valid year in both calendars and nothing could reliably guess which one you meant.

bs2ad/ad2bs accept the date either as YYYY-MM-DD or as "D Month YYYY" (e.g. "1 Baisakh 2083", "1 Jan 2000" or "1 January 2000") — the month name matched case-insensitively either way, so Shrawan, shrawan, and SHRAWAN all work. The BS side also accepts common romanisation variants (Baishakh alongside Baisakh, Sawan alongside Shrawan, ...) and Devnagari month names (साउन), not just the one spelling BS_MONTH_NAMES prints — calbs's month argument does the same.

today defaults to --script devnagari, so its BS line reads Devnagari without any flag; pass --script latin for the romanised form instead. The AD line is always Latin. bs2ad/ad2bs/range default to latin and need --script devnagari explicitly.

Interactive

Run nepkit with no arguments in a terminal and it clears the screen and opens a session:

$ nepkit
                      _     _  _
 _ __    ___   _ __  | | __(_)| |_
| '_ \  / _ \ | '_ \ | |/ /| || __|
| | | ||  __/ | |_) ||   < | || |_
|_| |_| \___| | .__/ |_|\_\|_| \__|
              |_|
nepkit v0.4.0 - Bikram Sambat (BS) <-> Gregorian (AD) date conversion
Today  14:32 NPT  BS 27 Shrawan 2083   AD 12 Aug 2026 14:32 Wed

Type a command, 'help', 'clear', or 'quit'.  Up/Down recalls history.

nepkit> today
BS २७ साउन २०८३ १४:३२ बुध
AD 12 Aug 2026 14:32 Wed

nepkit> bs2ad 2081-04-15
AD 30 Jul 2024 (2024-07-30) Tue

nepkit> quit

It accepts exactly the commands above — the same table, not a parallel interface — so anything you can type at the shell works here unchanged. A bad line reports the error and returns you to the prompt rather than ending the session. clear (or cls) wipes the screen and redraws the banner. quit, exit, q, and Ctrl-D all leave; Ctrl-C abandons the current line only.

Those four are prompt-only words, not subcommands — nepkit clear at a shell stays a usage error rather than clearing your terminal.

Up and Down recall previous commands, and the usual readline editing keys work (Ctrl-A, Ctrl-E, Ctrl-R, and so on). History lasts for the session and is not written to disk. CPython ships no readline on Windows, so nepkit depends on pyreadline3 there to supply one — it needs a real console, which Windows Terminal provides. Should it fail to load, the prompt still works; it loses the editing keys and says so in its banner.

Only on a terminal. With stdin redirected — a pipeline, a script, CI — nepkit prints help and exits 2 exactly as before, so nothing ever blocks waiting for a prompt that isn't there.

Calendars

$ nepkit calbs 2081 4
        Shrawan 2081
    16 Jul - 16 Aug 2024
Sun Mon Tue Wed Thu Fri Sat
          1   2   3   4   5
  6   7   8   9  10  11  12
 13  14  15  16  17  18  19
 20  21  22  23  24  25  26
 27  28  29  30  31  32

$ nepkit calad 2024 7
         July 2024
Ashadh 17 - Shrawan 16, 2081
Sun Mon Tue Wed Thu Fri Sat
      1   2   3   4   5   6
  7   8   9  10  11  12  13
 14  15  16  17  18  19  20
 21  22  23  24  25  26  27
 28  29  30  31

Shown here without a box: grids are boxed and coloured only on a real terminal, and plain like this when redirected — piped, or captured for this README — the same convention ls and git follow. See DEMO.md for what it looks like on a terminal. Force it either way with --color always|never|auto.

Both default to the current month. A Gregorian month never lines up with a BS month, so the subtitle names both ends of the span rather than pretending a single corresponding month exists.

The month argument accepts a name too, case-insensitively — calbs matches BS names (Shrawan/shrawan, a romanisation variant like Sawan, or a Devnagari name like साउन), calad matches English names or 3-letter abbreviations (July/Jul/jul) — alongside the numeric 1-12 form:

nepkit calbs 2081 Shrawan
nepkit calad 2024 Jul

Neither command takes --script — see Limitations below.

On a terminal, today's date is picked out in bold bright cyan — the bright form of the box's own border colour, so the highlight belongs to the grid containing it rather than arriving from a different palette. The highlight exists only in the coloured path: piped output is byte-for-byte identical whether or not today falls in the month shown, so nothing parsing stdout breaks on the one day a month it would otherwise appear. --json reports it as a today field instead, which is null when today is elsewhere.

Scripting

Every command takes --json:

$ nepkit ad2bs 2008-05-28 --json
{"bs": "2065-02-15", "ad": "2008-05-28", "weekday": "Wed"}

stdout carries results, stderr carries errors, and neither ever carries both. stdout contains no ANSI escapes unless you ask for colour explicitly, so piping is always safe.

Exit codes come straight from the exception hierarchy, so a script can branch without parsing any text:

Code Meaning Example
0 success
2 usage error — bad flag or unknown command nepkit nosuchcommand
3 not a real date nepkit bs2ad 2081-13-01
4 a real date, but outside the bundled range nepkit bs2ad 2095-01-01

The 3/4 split is the one that matters when scripting: 4 is worth retrying against another source, 3 never is. Collapsing both into 1 would throw that distinction away at exactly the boundary where it is most useful.

Supported range

Calendar From To
Bikram Sambat 2000-01-01 2090-12-30
Gregorian 1943-04-14 2034-04-13

That is 91 years, 33,238 days. Anything outside it raises DateOutOfRangeError rather than extrapolating, because there is no rule to extrapolate with — dates beyond the table would have to be invented.

The bounds are computed from the bundled data, not written down separately, so extending the table moves them automatically.

Design decisions

One anchor, everything else derived. The whole library hangs on a single verified correspondence: BS 2000-01-01 = AD 1943-04-14. That is the only Gregorian fact in the package that cannot be computed, because month lengths alone cannot tell you where the calendar sits against the Gregorian one. Every other bound — the last BS date, both ends of the AD window — is derived from it plus the table. A second hardcoded date would be free to drift out of sync, and the failure would be silent and total.

Both directions collapse to a day count. A BS date becomes "days since the anchor", integer arithmetic happens there, and the result expands out the other side. datetime.date is already a correct expander for the Gregorian side, so bs_to_ad is one line; the real work is the inverse, which has no equivalent in the standard library.

Types instead of validation where possible. ad_to_bs accepts a datetime.date rather than three integers, which removes an entire error class from its contract at no cost.

Human-text parsing and formatting are library code, not CLI code. The CLI used to own parse_bs_date/format_bs_date and friends as private helpers, which meant a library caller could not parse "1 Baisakh 2083" into a BSDate, or format one back into that form, at all -- only the CLI process could. They moved into the library so the CLI is provably a thin layer over it rather than a second implementation that has to be kept in sync by hand.

Limitations

  • Time of day works only in Nepal Standard Time (UTC+05:45). BSDateTime and bs_datetime_to_ad_datetime/ad_datetime_to_bs_datetime carry a naive time-of-day through the date conversion; nepkit does not convert between timezones. A tz-aware datetime is rejected outright rather than silently treated as NPT — convert it yourself with astimezone first. In the CLI, only today and the interactive banner show the time; there is no --time flag on bs2ad/ad2bs.
  • Devnagari formatting stops at the calendar grid. --script devnagari covers bs2ad/ad2bs/today/range and the library (nepkit.BS_MONTH_NAMES_NE, nepkit.format_bs_date/nepkit.bs_month_name's devnagari flag, and nepkit.render's grid functions); calbs and calad render Latin only. today defaults to --script devnagari (the others default to latin), and its AD line is Latin regardless of --script -- there is no Devnagari table for Gregorian month names, so the option only ever touches its BS line. See DEMO.md.
  • The range is hard-bounded at BS 2000–2090 and will not extrapolate.
  • Correctness rests on the data, and round-trip tests alone cannot prove it. The property suite verifies self-consistency exhaustively — every one of the 33,238 days round-trips, and consecutive day counts produce consecutive dates. But both directions read the same table, so a wrong month length cancels out exactly and every property still passes. This was verified by deliberately corrupting the table: all properties passed while conversions were silently wrong. Two things stand behind the numbers themselves instead: the independent sourcing in src/nepkit/data/DATA.md, and tests/test_oracle.py, which checks conversions against a handful of BS↔AD dates attested by sources with no connection to nepkit or to either source table. Both are a sample, not a proof — the oracle pairs cover select decades, and nothing past today can ever be externally attested, since the table's later years (toward BS 2090) haven't happened yet.

Data provenance

src/nepkit/data/DATA.md records where the calendar table came from: two independently maintained sources with different authors, languages, and conversion epochs, pinned at specific commits, diffed row by row over all 91 years with no disagreements, and cross-checked by walking each source forward from its own epoch to the anchor.

Development

This project uses uv.

git clone https://github.com/akakritagya/nepkit
cd nepkit
uv sync --group dev          # pytest, ruff, mypy, pre-commit
uv run pytest                # run tests
uv run ruff check .          # lint
uv run ruff format .         # format
uv run mypy                  # type check
uv run pre-commit install                         # lint/format/type-check on commit
uv run pre-commit install --hook-type commit-msg  # enforce Conventional Commits
uv run pre-commit install --hook-type pre-push    # full test suite before push

pre-commit runs lint, format, and type-check on every commit; blocks large files, private keys, and direct commits to main; runs the full pytest suite before push (not on every commit — too slow to survive contact with a growing suite); and enforces Conventional Commits on the message.

Releasing

Releases go to PyPI through Trusted Publishing — GitHub Actions proves the repository's identity over OIDC, so no API token exists to leak or rotate. Bump version in pyproject.toml, then:

git tag v0.2.0 && git push origin v0.2.0

The tag runs .github/workflows/publish.yml, which re-runs the full gate, refuses to continue if the tag disagrees with the packaged version, and only then uploads. Run the workflow manually to rehearse against TestPyPI first.

License

MIT — see LICENSE.

Metadata

Release files for nepkit 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for nepkit 0.4.0
File Size Uploaded
nepkit-0.4.0.tar.gz 37.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nepkit 0.4.0
File Interpreter ABI Platform
nepkit-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 77.6 kB

Release files / nepkit-0.4.0.tar.gz

Download URL nepkit-0.4.0.tar.gz
Size 37.2 kB
Tags Source
SHA-256 checksum
How to use checksums
9f4488c09cc221fa889fa74d937399703a368c21ab48e3a6437541657f47457f
BLAKE2b-256 checksum
How to use checksums
2020048e7b1af2f2492d15d6c6d25e9cbf6e0abfd7bf4d7838d6fe629f38f86a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release files / nepkit-0.4.0-py3-none-any.whl

Download URL nepkit-0.4.0-py3-none-any.whl
Size 40.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bb3b35383b20a43c67700c1b8c7a100462aea4f5ca3b7f9040a78826ba165393
BLAKE2b-256 checksum
How to use checksums
979c80e1b3b00a8fe9486d553c17130b6675a052ddea01451af970b00049d1ed
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release history Release notifications | RSS feed

0.4.1

2 release files

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page