I built two counters for the same thing — how many days a person spends in China in a tax year — and they disagreed by two days on every trip. Both were defensible. Only one matches the regulation.
The threshold itself is easy to remember: 183 days in a calendar year makes you a Chinese tax resident, which puts worldwide income in scope. Announcement No. 34 of 2019 is the document that says how to count those days, and it does not say "count every day you were there".
The counter everyone writes first
from datetime import date def days_inside(arrive: date, leave: date) -> int: """Every calendar day touched, boundaries included.""" return (leave - arrive).days + 1 For a Monday 08:00 arrival and a Friday 19:00 departure that returns 5. This is also how China's double tax treaties count days, which is what makes it feel obviously right. It is wrong for the domestic residency test.
What the text says
Article 2 of the announcement, translated from the official Chinese: a day spent inside China for a full 24 hours counts toward the residence total; a day spent inside China for less than 24 hours does not.
That single sentence drops both boundary days. The Monday landing and the Friday flight out are not China days. The same trip is three days: Tuesday, Wednesday, Thursday.
def counted_days(days_fully_inside: int) -> int: """Boundary days are excluded by construction.""" return days_fully_inside The two conventions differ by exactly two days per trip, at the two ends. On a commute they are most of the week:
| Trip | Domestic rule | Treaty count | Gap |
|---|---|---|---|
| Mon 08:00 in, Fri 19:00 out | 3 | 5 | 2 |
| Sat in, Sun out | 0 | 2 | 2 |
| 1 May in, 2 June out | 31 | 33 | 2 |
| 52 weekly commutes | 156 | 260 | 104 |
Pin the implementation to the worked example
When the rule came out, the finance ministry and the tax administration published a joint Q&A with an example: a cross-border commuter arriving Monday morning and leaving Friday evening gets three countable days a week, so 52 weeks gives 156 days, under the 183-day line, and stays a non-resident.
That example is the test I keep. It is short, it is in the government's own words, and it fails loudly if someone "fixes" the boundary handling:
weekly = 3 annual = weekly * 52 assert annual == 156 assert annual < 183 If a later refactor makes that 5 — the intuitive version — the assertion catches it before the bug reaches a payroll file.
Where the second counter is correct
The treaty route counts the days the domestic rule discards. The tax administration's commentary on the employment-income article says arrival and departure days, partial days, and weekends or holidays spent inside the country all count, and only days genuinely outside are subtracted. Fifty-two commuting weeks under that count is 260 days, well past 183.
The two counts answer different questions, which is why neither is a bug:
- the domestic count decides whether China taxes worldwide income for the year;
- the treaty count decides whether China may tax a salary at all, when the person is a tax resident elsewhere.
The part with no code, which is harder
Sitting on top of the 183-day test is a six-year rule. A non-domiciled resident keeps an exemption for foreign-source income paid by a foreign employer until six consecutive qualifying years have passed; after that the exemption stops. Three details matter and all three are arithmetic:
- The run starts at 2019. Earlier years were wiped, so 2025 was the first year in which six qualifying years could exist.
- A single absence of more than 30 days in any qualifying year resets the run. Thirty days does not; 31 does.
- The window is the six years immediately before the year being tested.
def worldwide_year(first_year, breaks, short_years, last=2040): def qualifies(y): return y >= first_year and y not in breaks and y not in short_years for y in range(first_year, last + 1): if qualifies(y) and all(qualifies(y - k) for k in range(1, 7)): return y | Profile | First year worldwide income is taxable |
|---|---|
| Arrived 2019, never away more than 30 days | 2025 |
| Arrived 2019, exactly 30 days away in 2022 | 2025 |
| Arrived 2019, 31 days away in 2022 | 2029 |
| Arrived 2019, one 170-day year in 2024 | 2031 |
| Arrived 2021, never away more than 30 days | 2027 |
The row that surprised me is the 170-day year. A year that falls short of 183 days removes itself from every window that contains it, and a window only reaches back six years. So one short year moves the worldwide test by exactly six years — not one, and not forever. That is worth an assertion of its own:
assert worldwide_year(2019, set(), set()) == 2025 assert worldwide_year(2019, set(), {2024}) == 2031 assert 2031 - 2025 == 6 Three assertions I would keep if I threw the rest away
-
3 * 52 == 156and156 < 183— the official example, reproduced. - A 30-day absence leaves the six-year run intact; 31 days resets it.
- One sub-183-day year shifts the worldwide-tax year by exactly six.
Everything else is documentation.
What this does not tell you
I am reading published rules, not giving tax advice. Three gaps I could not close from the primary documents: whether a 2026 announcement has modified any of these three texts (I found no replacement, but I did not page through the consolidated index of current rules); how a local tax office handles the record-filing requirement in practice; and which treaty wording applies to a given nationality, because China's treaty network uses both the calendar-year and the rolling-12-month forms.
The full write-up, with the clause numbers and the source links, is on my site: China's 183-day tax test: one trip, two day counts. The counting script is there too.
Drafted with an AI assistant from my own reading of the primary documents; every clause number and link here was checked against the source.

