A small command-line tool that reads a calendar feed and tells you how many days you have left in the Schengen Area under the 90/180 rule. Pure Python standard library, no dependencies.
- Reads an
.icsfile or subscribed calendar URL (iCloud, Google Calendar, Outlook, etc.). - Counts your Schengen days in the rolling 180-day window ending on a given date.
- Handles overlapping trips correctly (no double-counting shared days).
- Forecasts your balance at future dates so you can plan trips.
- Lets you test hypothetical trips without modifying your calendar.
- Scans your full date range for any 90/180 breaches.
This matches the official EU short-stay calculator — both entry and exit days count, and the window is the 180 days ending on your reference date.
- It's not a substitute for legal advice. Border officers exercise discretion.
- It doesn't track non-Schengen Europe (UK, Ireland, Cyprus). If your calendar contains trips to those, exclude them from your feed before counting.
- It doesn't sync, integrate with anything, or run in the background. You run it, it tells you a number, you close the terminal.
You'll need Python 3.10 or later. Clone the repo:
git clone https://github.com/YOUR_USERNAME/schengen-calculator.git
cd schengen-calculatorThat's it — there's nothing to install.
The first time you run it, it'll ask for a calendar source and offer to save it for next time:
python schengen.py
# Calendar source: webcal://p01-calendars.icloud.com/published/2/...
# Save this URL for future runs? [Y/n] yAfter that, run with no arguments to get the standard report:
python schengen.py# Default: rolling-180 status as of today
python schengen.py
# What will my balance be N days from now?
python schengen.py --in 60
# Forecast table at +0/+30/+60/+90/+180 days
python schengen.py --forecast
# Status on a specific date
python schengen.py --on 2026-09-15
# Test a hypothetical trip (does not save)
python schengen.py --check 2026-08-01 2026-08-20
# Add hypothetical trips that count toward all calculations (repeatable)
python schengen.py --add 2026-06-15 2026-06-22 --add 2026-07-10 2026-07-20
# Combine: with these added trips, can I also fit a 2-week trip in late August?
python schengen.py --add 2026-06-15 2026-06-22 --check 2026-08-15 2026-08-29
# Filter by event title (e.g. if you don't keep a dedicated Schengen calendar)
python schengen.py --tag "schengen"
# Manage the saved feed
python schengen.py --show-saved-feed
python schengen.py --clear-saved-feedYou can also pass a path or URL explicitly, which overrides the saved one:
python schengen.py /path/to/calendar.ics
python schengen.py "webcal://p01-calendars.icloud.com/published/2/..."In order:
- Positional argument (path or URL)
SCHENGEN_FEEDenvironment variable- Saved config at
~/.config/schengen/feed - Interactive prompt
The saved feed file is created with 0600 permissions (readable only by you). The URL itself is functionally a read-credential for your calendar — anyone with it can subscribe to your events. Don't commit it to a repo, paste it on Slack, or include it in screenshots.
- Right-click your Schengen-trips calendar in the sidebar.
- "Sharing settings…" → check "Public Calendar".
- Click the share-arrow icon next to the calendar name → "Copy URL".
You'll get a webcal:// URL. The script auto-rewrites this to https:// before fetching.
- Settings → "Settings for my calendars" → pick the calendar.
- Scroll to "Integrate calendar" → "Secret address in iCal format".
- Copy the URL ending in
.ics.
- Trips are read from the calendar feed. By default, every event in the feed counts as a Schengen trip — so you probably want a dedicated calendar for Schengen travel rather than your main one. Use
--tagto filter by event-title substring if you can't. - All-day events use exclusive end dates per RFC 5545 — an event with
DTSTART=20260503andDTEND=20260511counts as 3-10 May (8 days), not 11. - Overlapping or touching events are merged so a shared day is only counted once.
- The 180-day window is inclusive on both ends, so it covers exactly 180 days. Both your entry and exit days count as days in Schengen.
To make it accessible from anywhere as schengen, drop a wrapper somewhere on your PATH:
mkdir -p ~/bin
cat > ~/bin/schengen <<'EOF'
#!/bin/bash
exec python3 "$HOME/path/to/schengen-calculator/schengen.py" "$@"
EOF
chmod +x ~/bin/schengenMake sure ~/bin is on your PATH (export PATH="$HOME/bin:$PATH" in your shell config). Then schengen --forecast works from any directory.
- Romania and Bulgaria joined Schengen for air/sea borders in March 2024 and land borders in January 2025. Trips before those dates don't count toward the 90/180 limit. Croatia joined in January 2023.
- Cyprus and Ireland are not in Schengen. Don't include them.
- The script counts your departure and arrival days both as Schengen days. This matches the EU calculator. A trip 3-10 May counts as 8 days even if you only spent half of those days physically inside Schengen.
- Border officers can be stricter. The 90/180 rule has the force of law but is enforced day-by-day by humans with discretion. Don't run yourself down to zero on the assumption you have an absolute right to the 90th day.
- No caching. Each run fetches the URL fresh. Fast enough for occasional use; inefficient if you're scripting repeated calls.
The only data this script reads or transmits:
- Reads your calendar feed (locally, or via HTTPS GET to the URL you give it).
- Writes the URL to
~/.config/schengen/feedif you opt in to saving. - Writes nothing else, sends nothing anywhere else.
No analytics, no telemetry, no third parties.
MIT. See LICENSE.