This Python script generates an iCalendar (.ics) file from CSV data containing hunting season information. The calendar includes events for each hunting season with detailed information about the species, areas, and bag limits.
| Calendar | Description |
|---|---|
![]() |
![]() |
| This shows what your calendar would look like if you subscribed to the .ics cal in this repo. | This shows what you would see if you clicked into a date with 16 huntable species. |
Not legal advice. Season dates change every year and are amended mid-season. Always confirm against the current Ohio Hunting and Trapping Regulations before you go afield.
If your calendar supports subscribing to a calendar, you can use the URL of the .ics file directly.
If you happen to be a hunter in Ohio, you can subscribe to the calendar in this repo directly:
https://raw.githubusercontent.com/TheOutdoorProgrammer/huntercalgen/refs/heads/main/ohio.ics
ohio.ics always holds the current season, so the subscribe URL never changes
year to year. It is generated from ohio.csv.
The generated .ics file can also be downloaded and imported into most calendar applications:
- Google Calendar: In the web interface, go to Settings > Import & Export > Import
- Microsoft Outlook: File > Open & Export > Import/Export > Import an iCalendar (.ics) file
- Apple Calendar: File > Import
- Mobile devices: You can usually email the .ics file to yourself and open it to add it to your calendar
- Automatically groups consecutive days with identical hunting options
- Organizes species by scope and area
- Properly handles date ranges and day-of-week restrictions
- Creates calendar events with detailed descriptions for each hunting opportunity
- Emits stable, content-derived
UIDs so subscribers get updates in place instead of duplicates
The script generates an iCalendar (.ics) file with events representing hunting seasons. The events include:
- Summary: Number of species available on that date
- Description: Detailed information about each species, including:
- Species name
- Scope (if applicable)
- Area(s)
- Bag limit
The description has the format:
Species1 (Scope1: Area1, Area2 | Scope2: Area1): Bag Limit
Species2 (Scope1: Area1, Area2 | Scope2: Area1): Bag Limit
If the bag limit is different for a species, it will be shown separately. For example, in Ohio on January 9th 2027 you can trap river otter in zone a at a different bag limit than in zone b. The calendar will show:
River Otter (Trapping: Otter Zone A): 3/Season
River Otter (Trapping: Otter Zone B): 1/Season
However, if you were in a fantasy state where the bag limit was the same for both zones, it would show:
River Otter (Trapping: Otter Zone A, Otter Zone B): 3/Season
If there is no scope, the species will just show the area:
River Otter (Statewide): 3/Season
ohio.csv was transcribed from ODNR's 2026-27 Hunting and Trapping Season
Dates
and verified against the Ohio Administrative Code, which is the controlling law.
The Ohio Wildlife Council adopted these seasons on April 29, 2026; the rules took
effect June 30, 2026 (migratory birds) and July 6, 2026 (deer and small game).
⚠️ As of this writing ODNR has not published a 2026-27 regulations digest. The digest URL still serves the 2025-26 booklet, which expires August 31, 2026 — one day before this season opens. Anything sourced from that digest today is a season out of date. The OAC rules are the current authority.
The old Lake Erie Marsh / North / South zones are gone. Ohio used the USFWS five-year zone-selection window to draw new zones (OAC 1501:31-7-07, operative August 1, 2026). This is a genuine redraw, not a rename — do not assume Zone A is the old marsh zone.
| Zone | Counties |
|---|---|
| A | Auglaize, Defiance, Erie, Fulton, Hardin, Henry, Marion, Mercer, Ottawa, Paulding, Sandusky, Shelby, Van Wert, Williams, Wyandot; plus the part of Lucas outside Zone B |
| B | Allen, Ashtabula, Crawford, Cuyahoga, Geauga, Hancock, Huron, Lake, Lorain, Mahoning, Medina, Morrow, Portage, Putnam, Richland, Seneca, Summit, Trumbull, Wood; the part of Lucas south of U.S. 24 and west of I-75; parts of Ashland, Wayne, Holmes, Stark, and Columbiana; and all Lake Erie islands and waters not in Zone A |
| C | The remainder of the state |
Six counties are split and cannot be resolved at county level — Lucas (A/B) and Ashland, Wayne, Holmes, Stark, Columbiana (B/C). Check the rule's road boundaries.
Zones B and C share goose dates but have different duck dates. River otter
Zone A/B is an unrelated system covering eastern Ohio, which is why the calendar
labels those areas Otter Zone A/B rather than plain Zone A/B.
| Not in the calendar | Why |
|---|---|
| Ruffed grouse | Closed statewide. OAC 1501:31-15-02(S)(4) makes taking a grouse unlawful except on shooting preserves, dog-training grounds, field trials, or by written authorization. The four controlled hunts run under that exception and publish no dates. There was a general season in 2025-26; there is none now. |
| Spring 2027 wild turkey | Not yet adopted — ODNR lists it "to be announced" and no dates exist in the OAC. |
| Coyote, woodchuck | No closed season, so every day would carry them. |
| Falconry extended season | Separate permit and method (Sept 1 – March 10 under OAC 1501:31-15-13). |
- Lake Erie marsh trapping may be broader than listed. ODNR's chart extends only mink, muskrat, skunk, opossum, and weasel to March 15. OAC 1501:31-15-18(A)(6) reads as extending all furbearer trapping except fox in Lucas (east of the Maumee), Ottawa, Sandusky, and Erie counties — which would sweep in raccoon, beaver, and river otter. The calendar follows the chart, so it under-reports rather than putting you in a closed season.
- Paulding County deer limit is disputed. ODNR's bag limit map and press release say three; the authenticated text of OAC 1501:31-15-11 grants Paulding two. This does not affect any date, but confirm with the Division of Wildlife before taking a third deer there.
- Python 3.9+
- Required Python packages:
icalendar
Everything else (csv, argparse, datetime, hashlib) is standard library.
You can install the required package using pip:
pip install -r requirements.txtThe input CSV file must contain the following columns:
| Column Name | Description | Example Values |
|---|---|---|
| Species | Animal species name | Deer, Turkey, Squirrel |
| Scope | Optional subcategory of the species | Archery, Youth Gun, Roosters Only |
| Area | Geographic hunting area | Statewide, 16 Counties, Waterfowl Zone A |
| Opening Date | Season start date (MM/DD/YYYY or MM/DD/YY) | 09/26/2026 |
| Closing Date | Season end date (MM/DD/YYYY or MM/DD/YY) | 02/07/2027 |
| Days Of Week | Days when hunting is allowed | ALL, F:SA:SU (Friday, Saturday, Sunday) |
| Limit | Bag limit information | 4/Day, 1/Season, See Bag Limit Map |
Malformed input is rejected with the offending row number rather than silently producing a wrong calendar: unparseable dates, unknown day-of-week tokens, a closing date before its opening date, a missing column, or an empty species all fail the run.
Species,Scope,Area,Opening Date,Closing Date,Days Of Week,Limit
Deer,Archery,Statewide,09/26/2026,02/07/2027,ALL,See Bag Limit Map
Turkey,Fall,70 Counties,10/01/2026,10/25/2026,ALL,1/Season
Crow,,Statewide,06/05/2026,03/07/2027,F:SA:SU,NoneALL: Hunting allowed every day of the week- Individual days are specified with a colon-separated list:
M: MondayTU: TuesdayW: WednesdayTH: ThursdayF: FridaySA: SaturdaySU: Sunday
- Example:
F:SA:SUmeans hunting is allowed only on Friday, Saturday, and Sunday
python csv2ical.py input_file.csvThis will generate a file named hunting_calendar.ics in the current directory.
python csv2ical.py input_file.csv --output my_calendar.icsThis will generate a file named my_calendar.ics.
python csv2ical.py input_file.csv --output my_calendar.ics --name "Ohio Hunting 2026-27"--name sets what subscribers see in their calendar list. Without it the
calendar is labeled Hunting Calendar.
Events carry a DTSTAMP of the current time, so regenerating an unchanged
calendar still produces a diff. Set SOURCE_DATE_EPOCH to pin it and get
byte-identical output:
SOURCE_DATE_EPOCH=1700000000 python csv2ical.py input_file.csv --output my_calendar.icsUIDs are always derived from event content, so they are stable regardless.

