in Income Tax

Automating my ITR-3 filing: generating the tax-return JSON from a spreadsheet

Every year around July–August, the same ritual: open the Income Tax Department’s offline ITR utility and spend an evening (or three) typing numbers into a maze of schedules — Profit & Loss, Balance Sheet, depreciation blocks, capital gains, interest, TDS, the works. As a software-development freelancer running a proprietorship, I file ITR-3, which is about as involved as individual returns get.

This year I finally did what I should have done years ago: I automated it. The result is a small open-source tool that takes the data I already have and spits out the exact JSON the tax portal expects. I still review everything in the official utility before filing — but the tedious data-entry is gone.

Here’s the whole story, including the engineering problems that turned out to be more interesting than I expected.

Repo: https://github.com/pankajbatra/income-tax-itr3-proprietor

⚠️ This is an unofficial helper, provided as-is, and it is not tax advice.
The official utility remains the source of truth — always import, review, and verify before filing.

The problem

The offline ITR utility exports (and imports) a single big JSON file — a full ITR-3 with ~34 schedules. If you’ve ever looked inside one, it’s a deeply nested structure with thousands of fields: personal info, filing status, salary, house property, business P&L, a trading account, depreciation schedules (DPM, DOA, DEP), capital gains, other sources, Chapter VI-A, Part B-TI, Part B-TTI, carry-forward losses, AMT credit, foreign assets, special-rate tax tables… and on and on.

Most of these numbers already live in two places:

  1. My accounting spreadsheet — the P&L and Balance Sheet I maintain through the year, plus capital gains and depreciation working.
  2. The portal’s “prefill” data — a JSON you can download after login that already contains your PAN, bank accounts, TAN-wise TDS from Form 26AS, advance-tax challans, foreign assets, unlisted shares, directorships, prior carry-forward losses, and more.

So the data exists. It’s just scattered, and the utility makes you re-key it by hand. Perfect job for a script.

The key realisation: you can’t build the JSON from scratch

My first instinct was “read the spreadsheet, emit the JSON.” That doesn’t work, and understanding why shaped the whole design.

A huge fraction of the upload JSON — bank IFSC/account numbers, TAN-wise TDS rows, challan BSR codes and serial numbers, foreign-asset details, unlisted-share holdings, the fixed special-rate tax tables — exist nowhere in a spreadsheet. It can only come from your prior return or the portal’s prefill.

Conversely, the current year’s financials (this year’s P&L, depreciation, capital gains) aren’t in the prefill.

So the tool is built around three sources:

SourceProvides
Prefill JSON (you download it)all account-level data: personal info, banks, TDS, challans, foreign assets, holdings, house property, regime, GSTIN, opening depreciation WDV, nature of business
A spreadsheet template (you fill it)current-year financials: P&L, income heads, depreciation, capital gains, GST turnover, salary, balance sheet
A fixed “skeleton” JSON (ships with the tool)The non-personal ITR-3 scaffolding — rate tables, empty-schedule structure, form metadata

The tool merges the three and writes the upload JSON. The official utility does the final re-verification on import.

Reverse-engineering the format

There’s no public spec for the upload JSON’s semantics (there is a JSON Schema, more on that later, but it only describes shapes, not meaning). So I did it the old-fashioned way: I filled a return in the utility, exported the JSON, and diffed it against my spreadsheet to map every field.

A few things I learned:

  • The prefill uses a completely different schema from the upload JSON — different key names, different casing (WdvfirstDay vs WDVFirstDay), different nesting. So a big part of the tool is a translation layer from prefill-shape to upload-shape.
  • The prefill’s lastFiledITR block carries forward most of the account-level data I feared I’d have to maintain in a template — foreign assets, unlisted shares, directorships, house-property co-owners, opening depreciation WDV. That was the moment I realised a personal “template JSON” wasn’t needed at all; the fresh prefill supplies it every year.

The Digest you can’t reproduce

Every exported JSON has a Digest field — a base64 hash. I spent a little while trying to reproduce it (SHA-256 over various canonicalisations, with and without the digest field, sorted keys, compact separators…). None matched. The utility computes it with its own internal canonicalisation, and there’s no need to replicate it: the utility recomputes and signs the digest on import. So the tool writes a placeholder and lets the utility handle it. Sometimes the right engineering answer is “don’t.”

Reading a spreadsheet reliably

I wanted the input to be a spreadsheet, because that’s how I already think about these numbers, and because a non-programmer could use it too. But reading a spreadsheet robustly has sharp edges:

  • Named cells, not coordinates. The tool reads values by defined names (salary_gross, ltcg_sale, …), so you can rename labels, insert rows, or restyle the sheet without breaking anything.
  • Formulas without cached values. If you edit the workbook with a library like openpyxl (as the tool sometimes does), it strips the cached results of formula cells — so reading a formula-driven input cell returns None. The fix: if a needed cell is a formula with no cached value, evaluate the workbook on the fly with the formulas package. This makes the reader robust whether or not you last saved in Excel.

The template ships with several read-only, formula-driven sheets — a proper two-sided P&L, a Balance Sheet with the capital-account reconciliation inline, a Computation of Income with the tax and interest, and an Internal sheet with profitability ratios. The tool ignores these entirely (it recomputes from the raw inputs); they exist purely so I can eyeball the statements before I generate anything.

The balance sheet must actually balance

A proprietor’s balance sheet has to tie: sources of funds = application of funds. The tool derives the closing capital as the balancing figure and, crucially, refuses to generate the JSON if the Capital Account doesn’t reconcile. The reconciliation is the classic one:

opening capital
+ net profit
+ income credited to capital (salary, interest, dividend, capital gains)
+ exempt additions (PPF interest, gifts, tax refund)
− drawings
− taxes paid (TDS + advance tax) ← self-assessment tax is paid after 31 March, so it isn't a 31-March capital deduction
= closing capital

If that doesn’t match the asset-side balancing figure, a red “Difference — must be 0” cell lights up in the sheet, and the tool aborts with a clear message. It’s a nice forcing function: if your books don’t balance, you find out before you file, not after.

The most interesting bug: 234C interest

This one’s my favourite. Section 234C charges interest for underpaying advance tax in quarterly instalments. But it has a deferment relief: you’re not penalised for shortfalls caused by capital gains or dividend income you couldn’t reasonably have estimated early in the year — provided you pay the tax on it in later instalments or by 31 March.

I implemented the relief and compared my output to the utility’s. My number was ₹14,847; the utility said ₹13,157. A ₹1,690 gap. Everything else matched to the rupee, so this nagged at me.

The bug was subtle. To apply the relief, you deduct the tax attributable to the dividend income from each instalment’s requirement. I was computing that tax at the average rate (total tax ÷ total income ≈ 21%). But dividend income sits at the top of your income — it’s taxed at the marginal rate. The correct figure is tax(total) − tax(total − dividend), which for income in the 30% slab is 31.2% including cess, not 21%.

Using the marginal rate, each instalment’s relief grew, the shortfalls shrank, and the total landed on exactly ₹13,157 — matching the utility to the rupee:

Instalment (due)shortfallinterest
15 Jun (×3 months)18,800564
15 Sep (×3 months)1,09,8003,294
15 Dec (×3 months)2,02,1006,063
15 Mar (×1 month)3,23,6003,236
Total₹13,157 ✓

It’s the kind of detail you’d never notice unless you insisted on a rupee-perfect match — which, for tax, you should.

Validating against the official schema

The department publishes a JSON Schema for ITR-3. The tool can validate its output against it (--schema), which catches structural mistakes — a missing enum value, an empty field that should be non-empty, a null where a string is required. One gotcha: the schema is draft-04, and using a draft-07 validator misreads exclusiveMinimum and throws hundreds of false positives. Auto-detecting the draft fixed it. After a handful of real fixes (a foreign company legitimately has no PAN, so that key must be omitted rather than set to null), the output validates clean.

Using it

The workflow is short:

  1. Download your Prefill JSON from the e-filing portal (e-File → Income Tax Returns → Download Pre-filled Data).
  2. Fill the spreadsheet — type only in the yellow cells. Fix any red “must be 0” check.
  3. Run the tool (command below).
  4. Import output.json into the offline utility, review every schedule, and file.
python3 build_itr.py \
--prefill YOURPAN-Prefill-*.json \
--xls ITR_Input_Template.xlsx \
--schema schema/ITR-3_2026_Main_V1.1.json \
--out output.json

The repo ships with a fictional but internally-consistent example (John Doe, ABCDE1234F) so you can run the whole thing end-to-end before touching your own data.

Scope and honest limitations

It targets the common freelancer/doctors, income from profession and proprietorship firms/business case: ITR-3, new tax regime, one salaried employer, one house property, business/profession income, LTCG u/s 112A, interest and dividend, and a single-proprietor balance sheet. It’s built for AY 2026-27 — slab rates, the special-rate table and the schema change yearly.

It does not yet handle the old regime, presumptive income (44AD/44ADA — so if you file ITR-4 under 44ADA, you don’t need this), F&O, crypto/VDA, or multiple businesses. Those are all doable; PRs welcome.

And to be clear one more time: it’s an unofficial helper, not tax advice. It produces a draft you import into the official utility, which remains the authority. It computes 234A/234B exactly and estimates 234C (with the marginal-rate relief above), but the utility’s final numbers win.

Why open-source it

Nearly every software freelancer I know files essentially the same return with the same handful of income heads. If the tool saves them an evening of soul-crushing data entry — and lets a few of them contribute old-regime or presumptive support — that’s a good trade. The scaffolding is generic; the only per-person data lives in your prefill and your spreadsheet, which never go into the repo.

If you file ITR-3 as a proprietor, give it a try — and tell me what breaks.

Code: https://github.com/pankajbatra/income-tax-itr3-proprietor

Facebook Comments

Write a Comment

Comment