Error handling

The code classification of IztroError, what triggers it, the message format, and handling patterns.

Every entry point taking external input raises IztroError on invalid arguments. Date format, date existence, the year range and the hour index are validated up front in the core; string values such as gender, language, keys and configuration are validated in the binding layer.

from x_iztro import IztroError

IztroError subclasses ValueError, so an existing except ValueError still catches it and no code has to change on upgrade.

code

The exception message is written for humans and its wording may be adjusted between versions; branch on .code in your programs:

codeMeaningTypical trigger
invalid_dateIllegal dateBad format, a date that does not exist, outside 1583–9999
invalid_time_indexHour index out of rangeNot within 0–12
invalid_argumentAny other illegal argument or configurationGender, language, star key, palace name, custom tables
internalA defect inside the libraryShould be reported as a bug

These four values are the same set as Rust's IztroError::code() and Go's iztro.Error.Code, so cross-language branching logic transfers verbatim.

from x_iztro import Astro, IztroError

for args in [("2000-2-30", 2, "female"), ("2000-8-16", 13, "female"), ("2000-8-16", 2, "x")]:
    try:
        Astro().by_solar(*args)
    except IztroError as e:
        print(e.code, "|", e)

Output

invalid_date | invalid solar date '2000-2-30': day is out of range for that month
invalid_time_index | time_index must be 0-12, got 13
invalid_argument | invalid gender 'x': expected 'male' or 'female'

No PanicException is ever raised

A panic caused by a defect inside the library is caught in the binding layer and converted into an IztroError with the code internal; a pyo3_runtime.PanicException, which except Exception cannot catch, never escapes.


SituationExampleMessage
The format is not YYYY-M-D"2000/8/16"expected 'YYYY-M-D'
Year, month or day is not a number"abc-8-16"year is not a number
Month out of range"2000-13-1"month must be within 1-12
That month has no such day"2000-2-30"day is out of range for that month
Year outside the supported range"1500-1-1"year must be within 1583-9999

Lunar-only (reachable through by_lunar and the two *_by_lunar_date queries):

SituationExampleMessage
That lunar year has no such monthA month missing from the tablemonth does not exist in that lunar year
That lunar month has no such day"2000-7-30" (a short seventh month)day is out of range for that lunar month

Lunar messages are prefixed invalid lunar date '<original string>': and solar ones invalid solar date '<original string>': , so the message alone tells you which entry point was used.

Example

from x_iztro import Astro

for date in ["2000-13-1", "2000-2-30", "1500-1-1"]:
    try:
        Astro().by_solar(date, 2, "female")
    except IztroError as e:
        print(e)

Output

invalid solar date '2000-13-1': month must be within 1-12
invalid solar date '2000-2-30': day is out of range for that month
invalid solar date '1500-1-1': year must be within 1583-9999

The message carries the original input, so batch jobs can pinpoint which record failed.

Edge cases and pitfalls


Hour index

Trigger An hour index outside 0–12.

Example

try:
    Astro().by_solar("2000-8-16", 13, "female")
except IztroError as e:
    print(e)

Output

time_index must be 0-12, got 13

Edge cases and pitfalls

Thirteen values, not twelve

The Zi hour straddles midnight and splits into the early Zi hour (index 0) and the late Zi hour (index 12), so there are 13 legal values. To convert from a clock hour use utils.time_to_index, which is guaranteed to land in the legal range.


Gender and language

The code is invalid_argument in both cases.

ParameterLegal valuesMessage
gender"male" / "female"invalid gender 'x': expected 'male' or 'female'
languageThe six language codesinvalid language 'xx': expected one of zh-CN, zh-TW, en-US, ja-JP, ko-KR, vi-VN

Language codes are case-insensitive; both "zh-cn" and "zh-CN" are accepted. Passing the Gender / Language enum members instead lets the IDE stop a typo where it is written.


The utility and star-placement functions take language-independent keys, and an unknown key is an error:

from x_iztro import utils

try:
    utils.get_brightness("nosuchstar", 0)
except IztroError as e:
    print(e.code, "|", e)

Output

invalid_argument | unknown star key 'nosuchstar'

Passing a translated name is an error

These functions recognize keys, not translated names. Passing "紫微" gives unknown star key '紫微' — convert with i18n.key_of first.

The query methods on a chart (chart.palace, chart.star, palace.has) follow a different rule: they accept both keys and translations in the current language, and on no match they silently return None / False rather than raising. See the astrolabe object.


The six switches and the two custom tables of ChartConfig are all validated during charting, and the code is invalid_argument throughout:

SituationMessage
An unknown switch valueinvalid yearDivide 'nope': expected 'normal' or 'exact'
An unknown stem key in the mutagen tableinvalid mutagens key 'nope': unknown heavenly stem
A stem's mutagens are not four entriesinvalid mutagens for 'jiaHeavenly': expected 4 stars (lu, quan, ke, ji), got 1
A star's brightness table is not twelve entriesinvalid brightness for 'ziweiMaj': expected 12 entries, got 1

The lengths are strictly validated: mutagens must be exactly four entries and brightness exactly twelve, and one entry too many or too few is an error. The custom tables take keys only, never translated names.


Handling patterns

Skip bad rows in a batch

rows = [
    {"date": "2000-8-16", "ti": 2, "gender": "female"},
    {"date": "2000-2-30", "ti": 2, "gender": "female"},
    {"date": "1990-3-3", "ti": 13, "gender": "male"},
]

charts, failed = [], []
astro = Astro()

for row in rows:
    try:
        charts.append(astro.by_solar(row["date"], row["ti"], row["gender"], language="en-US"))
    except IztroError as e:
        failed.append((row["date"], e.code))

print(len(charts), failed)

Output

1 [('2000-2-30', 'invalid_date'), ('1990-3-3', 'invalid_time_index')]

Convert to your own exception type

class ChartError(Exception):
    def __init__(self, code: str, message: str):
        super().__init__(message)
        self.code = code


def build(date: str, ti: int, gender: str):
    try:
        return Astro().by_solar(date, ti, gender, language="en-US")
    except IztroError as e:
        raise ChartError(e.code, f"charting failed: {e}") from e


try:
    build("2000-2-30", 2, "female")
except ChartError as e:
    print(e.code, "|", e)

Output

invalid_date | charting failed: invalid solar date '2000-2-30': day is out of range for that month

Once code is carried outward, the layer above never has to parse the message text.

On this page