[{"data":1,"prerenderedAt":948},["ShallowReactive",2],{"page-\u002Fproperty-based-fuzz-testing-strategies\u002Fdesigning-strategies-for-domain-data\u002Fconstraining-dates-and-timezones-in-strategies\u002F":3},{"id":4,"title":5,"body":6,"description":911,"extension":912,"meta":913,"navigation":81,"path":944,"seo":945,"stem":946,"__hash__":947},"content\u002Fproperty-based-fuzz-testing-strategies\u002Fdesigning-strategies-for-domain-data\u002Fconstraining-dates-and-timezones-in-strategies\u002Findex.md","Constraining Dates and Timezones in Strategies",{"type":7,"value":8,"toc":900},"minimark",[9,18,21,26,55,59,295,413,417,431,438,442,485,489,492,498,504,510,516,519,596,600,603,646,649,751,755,762,816,819,823,832,852,858,862,891,896],[10,11,12,13,17],"p",{},"Most date bugs live at boundaries: the last day of a month, a leap day, the hour that does not exist when clocks spring forward, the hour that happens twice when they fall back, a timezone whose offset changed in a year the code did not expect. A test that uses ",[14,15,16],"code",{},"datetime(2026, 6, 15, 12, 0)"," exercises none of them. A property test with a well-constrained datetime strategy exercises all of them within a few hundred examples, and shrinks any failure to the single boundary that caused it.",[10,19,20],{},"The constraints are what make it work. An unconstrained strategy generates dates in the year 1 and times in timezones the application never serves, producing failures about database column limits rather than calendar logic. Bounding the range, choosing real timezones, constructing ordered ranges rather than filtering them, and deliberately biasing toward the boundaries turns the strategy into a calendar-bug finder.",[22,23,25],"h2",{"id":24},"prerequisites","Prerequisites",[27,28,29,40,47],"ul",{},[30,31,32,35,36,39],"li",{},[14,33,34],{},"hypothesis >= 6.100"," and Python 3.9+ for ",[14,37,38],{},"zoneinfo",".",[30,41,42,43,46],{},"The system's tzdata available — on minimal containers install the ",[14,44,45],{},"tzdata"," package.",[30,48,49,50,39],{},"The composition techniques in ",[51,52,54],"a",{"href":53},"\u002Fproperty-based-fuzz-testing-strategies\u002Fdesigning-strategies-for-domain-data\u002F","designing strategies for domain data",[22,56,58],{"id":57},"solution","Solution",[60,61,66],"pre",{"className":62,"code":63,"language":64,"meta":65,"style":65},"language-python shiki shiki-themes github-light github-dark","import datetime as dt\n\nfrom hypothesis import given, strategies as st\n\nMIN = dt.datetime(2020, 1, 1)\nMAX = dt.datetime(2035, 12, 31)\n\n# Aware datetimes in real IANA zones, including DST gaps and folds.\nmoments = st.datetimes(min_value=MIN, max_value=MAX, timezones=st.timezones())\n\n\n@st.composite\ndef date_ranges(draw, max_days=400):\n    start = draw(moments)\n    length = draw(st.timedeltas(min_value=dt.timedelta(0),\n                                max_value=dt.timedelta(days=max_days)))\n    return start, start + length                  # ordered by construction\n\n\n# Bias: a quarter of draws land on month ends, where billing bugs live.\nmonth_ends = st.builds(\n    lambda y, m: (dt.date(y, m % 12 + 1, 1) if m \u003C 12 else dt.date(y + 1, 1, 1))\n    - dt.timedelta(days=1),\n    st.integers(2020, 2035), st.integers(1, 12),\n)\ndates = st.one_of(month_ends, st.dates(min_value=MIN.date(), max_value=MAX.date()))\n\n\n@given(date_ranges())\ndef test_billing_periods_cover_the_range_exactly(r):\n    start, end = r\n    periods = split_into_billing_periods(start, end)\n    assert periods[0].start == start and periods[-1].end == end\n    assert all(a.end == b.start for a, b in zip(periods, periods[1:]))   # no gaps\n\n\n@given(dates)\ndef test_next_billing_date_is_always_later(d):\n    assert next_billing_date(d) > d\n","python","",[14,67,68,76,83,89,94,100,106,111,117,123,128,133,139,145,151,157,163,169,174,179,185,191,197,203,209,215,221,226,231,237,243,249,255,261,267,272,277,283,289],{"__ignoreMap":65},[69,70,73],"span",{"class":71,"line":72},"line",1,[69,74,75],{},"import datetime as dt\n",[69,77,79],{"class":71,"line":78},2,[69,80,82],{"emptyLinePlaceholder":81},true,"\n",[69,84,86],{"class":71,"line":85},3,[69,87,88],{},"from hypothesis import given, strategies as st\n",[69,90,92],{"class":71,"line":91},4,[69,93,82],{"emptyLinePlaceholder":81},[69,95,97],{"class":71,"line":96},5,[69,98,99],{},"MIN = dt.datetime(2020, 1, 1)\n",[69,101,103],{"class":71,"line":102},6,[69,104,105],{},"MAX = dt.datetime(2035, 12, 31)\n",[69,107,109],{"class":71,"line":108},7,[69,110,82],{"emptyLinePlaceholder":81},[69,112,114],{"class":71,"line":113},8,[69,115,116],{},"# Aware datetimes in real IANA zones, including DST gaps and folds.\n",[69,118,120],{"class":71,"line":119},9,[69,121,122],{},"moments = st.datetimes(min_value=MIN, max_value=MAX, timezones=st.timezones())\n",[69,124,126],{"class":71,"line":125},10,[69,127,82],{"emptyLinePlaceholder":81},[69,129,131],{"class":71,"line":130},11,[69,132,82],{"emptyLinePlaceholder":81},[69,134,136],{"class":71,"line":135},12,[69,137,138],{},"@st.composite\n",[69,140,142],{"class":71,"line":141},13,[69,143,144],{},"def date_ranges(draw, max_days=400):\n",[69,146,148],{"class":71,"line":147},14,[69,149,150],{},"    start = draw(moments)\n",[69,152,154],{"class":71,"line":153},15,[69,155,156],{},"    length = draw(st.timedeltas(min_value=dt.timedelta(0),\n",[69,158,160],{"class":71,"line":159},16,[69,161,162],{},"                                max_value=dt.timedelta(days=max_days)))\n",[69,164,166],{"class":71,"line":165},17,[69,167,168],{},"    return start, start + length                  # ordered by construction\n",[69,170,172],{"class":71,"line":171},18,[69,173,82],{"emptyLinePlaceholder":81},[69,175,177],{"class":71,"line":176},19,[69,178,82],{"emptyLinePlaceholder":81},[69,180,182],{"class":71,"line":181},20,[69,183,184],{},"# Bias: a quarter of draws land on month ends, where billing bugs live.\n",[69,186,188],{"class":71,"line":187},21,[69,189,190],{},"month_ends = st.builds(\n",[69,192,194],{"class":71,"line":193},22,[69,195,196],{},"    lambda y, m: (dt.date(y, m % 12 + 1, 1) if m \u003C 12 else dt.date(y + 1, 1, 1))\n",[69,198,200],{"class":71,"line":199},23,[69,201,202],{},"    - dt.timedelta(days=1),\n",[69,204,206],{"class":71,"line":205},24,[69,207,208],{},"    st.integers(2020, 2035), st.integers(1, 12),\n",[69,210,212],{"class":71,"line":211},25,[69,213,214],{},")\n",[69,216,218],{"class":71,"line":217},26,[69,219,220],{},"dates = st.one_of(month_ends, st.dates(min_value=MIN.date(), max_value=MAX.date()))\n",[69,222,224],{"class":71,"line":223},27,[69,225,82],{"emptyLinePlaceholder":81},[69,227,229],{"class":71,"line":228},28,[69,230,82],{"emptyLinePlaceholder":81},[69,232,234],{"class":71,"line":233},29,[69,235,236],{},"@given(date_ranges())\n",[69,238,240],{"class":71,"line":239},30,[69,241,242],{},"def test_billing_periods_cover_the_range_exactly(r):\n",[69,244,246],{"class":71,"line":245},31,[69,247,248],{},"    start, end = r\n",[69,250,252],{"class":71,"line":251},32,[69,253,254],{},"    periods = split_into_billing_periods(start, end)\n",[69,256,258],{"class":71,"line":257},33,[69,259,260],{},"    assert periods[0].start == start and periods[-1].end == end\n",[69,262,264],{"class":71,"line":263},34,[69,265,266],{},"    assert all(a.end == b.start for a, b in zip(periods, periods[1:]))   # no gaps\n",[69,268,270],{"class":71,"line":269},35,[69,271,82],{"emptyLinePlaceholder":81},[69,273,275],{"class":71,"line":274},36,[69,276,82],{"emptyLinePlaceholder":81},[69,278,280],{"class":71,"line":279},37,[69,281,282],{},"@given(dates)\n",[69,284,286],{"class":71,"line":285},38,[69,287,288],{},"def test_next_billing_date_is_always_later(d):\n",[69,290,292],{"class":71,"line":291},39,[69,293,294],{},"    assert next_billing_date(d) > d\n",[296,297,300,409],"figure",{"className":298},[299],"diagram",[301,302,309,310,309,314,309,318,309,326,309,336,309,342,309,348,309,353,309,356,309,359,309,363,309,366,309,372,309,377,309,381,309,384,309,387,309,390,309,393,309,395,309,398,309,404],"svg",{"viewBox":303,"role":304,"ariaLabelledBy":305,"xmlns":308},"0 0 820 262","img",[306,307],"dtz-t","dtz-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[311,312,313],"title",{"id":306},"Where calendar bugs hide",[315,316,317],"desc",{"id":307},"A timeline of calendar boundaries: month ends, the leap day, a spring-forward gap where a local hour does not exist, an autumn fold where a local hour repeats, and a year end. A mid-month noon example touches none of them; a bounded timezone-aware strategy biased toward boundaries reaches all of them.",[319,320],"rect",{"x":321,"y":321,"width":322,"height":323,"rx":324,"fill":325},"0","820","262","14","#fffdf8",[327,328,335],"text",{"x":329,"y":330,"textAnchor":331,"fontSize":332,"fontWeight":333,"fill":334},"410","28","middle","16","700","#3d405b","The interesting dates are the boundaries",[71,337],{"x1":338,"y1":339,"x2":340,"y2":339,"stroke":334,"strokeWidth":341},"40","140","780","1.4",[343,344],"circle",{"cx":345,"cy":339,"r":346,"fill":347},"100","8","#e07a5f",[327,349,352],{"x":345,"y":350,"textAnchor":331,"fontSize":351,"fill":334},"118","11","31 Jan",[343,354],{"cx":355,"cy":339,"r":346,"fill":347},"190",[327,357,358],{"x":355,"y":350,"textAnchor":331,"fontSize":351,"fill":334},"29 Feb",[343,360],{"cx":361,"cy":339,"r":346,"fill":362},"300","#f2cc8f",[327,364,365],{"x":361,"y":350,"textAnchor":331,"fontSize":351,"fill":334},"DST gap",[327,367,371],{"x":361,"y":368,"textAnchor":331,"fontSize":369,"fill":370},"172","10.5","#8a5a00","01:30 does not exist",[343,373],{"cx":374,"cy":339,"r":375,"fill":376},"420","6","#81b29a",[327,378,380],{"x":374,"y":350,"textAnchor":331,"fontSize":351,"fill":379},"#2a5f49","15 Jun 12:00",[327,382,383],{"x":374,"y":368,"textAnchor":331,"fontSize":369,"fill":379},"the usual example",[343,385],{"cx":386,"cy":339,"r":346,"fill":362},"560",[327,388,389],{"x":386,"y":350,"textAnchor":331,"fontSize":351,"fill":334},"DST fold",[327,391,392],{"x":386,"y":368,"textAnchor":331,"fontSize":369,"fill":370},"01:30 happens twice",[343,394],{"cx":333,"cy":339,"r":346,"fill":347},[327,396,397],{"x":333,"y":350,"textAnchor":331,"fontSize":351,"fill":334},"31 Dec",[319,399],{"x":338,"y":400,"width":401,"height":338,"rx":402,"fill":325,"stroke":403,"strokeWidth":341},"200","740","9","rgba(61,64,91,0.35)",[327,405,408],{"x":329,"y":406,"textAnchor":331,"fontSize":407,"fill":334},"225","11.5","Hand-written examples cluster in the green; the bugs cluster in the coral and gold.",[410,411,412],"figcaption",{},"A strategy biased toward the coloured points reaches every boundary within a few hundred examples.",[22,414,416],{"id":415},"why-this-works","Why this works",[10,418,419,422,423,426,427,430],{},[14,420,421],{},"st.datetimes"," with ",[14,424,425],{},"timezones=st.timezones()"," draws a naive local time within the bounds and a real IANA zone, then attaches the zone. Because the local time is generated independently of the zone's rules, some draws land in a DST gap — a local time that never occurred — or a fold, where it occurred twice and the ",[14,428,429],{},"fold"," attribute decides which. Those are exactly the values that break naive arithmetic, and Hypothesis's shrinker reduces any failure to the simplest such case.",[10,432,433,434,437],{},"Constructing ranges from a start and a non-negative duration guarantees validity without discarding draws, and shrinks toward a zero-length range at the earliest permitted date — a minimal, obviously-boundary counterexample. Biasing with ",[14,435,436],{},"st.one_of"," changes the distribution without narrowing it: month ends appear often, ordinary dates still appear, and nothing valid is excluded. Without the bias, a uniform draw over fifteen years lands on a month end roughly one time in thirty, which means a property run with a hundred examples may test only three or four of them.",[22,439,441],{"id":440},"edge-cases-and-failure-modes","Edge cases and failure modes",[27,443,444,451,460,473,479],{},[30,445,446,450],{},[447,448,449],"strong",{},"Unbounded ranges."," Years near 1 or 9999 overflow when a timedelta is added, and fail on database columns. Bound to the system's real range.",[30,452,453,459],{},[447,454,455,458],{},[14,456,457],{},"allow_imaginary=False"," by reflex."," Excluding non-existent local times hides the DST bugs the strategy exists to find. Only exclude them when the specification says those inputs are rejected upstream.",[30,461,462,465,466,469,470,472],{},[447,463,464],{},"Missing tzdata."," Slim containers lack the IANA database, so ",[14,467,468],{},"st.timezones()"," fails or draws only UTC. Install ",[14,471,45],{}," in the test image.",[30,474,475,478],{},[447,476,477],{},"Comparing aware and naive values."," Python raises on the comparison. Generate one kind per property and convert explicitly.",[30,480,481,484],{},[447,482,483],{},"Assuming a day is 24 hours."," Across a DST change a local day is 23 or 25 hours. Properties about \"the next day\" should use calendar arithmetic, not a fixed timedelta.",[22,486,488],{"id":487},"properties-worth-stating-about-calendar-code","Properties worth stating about calendar code",[10,490,491],{},"Generating the right dates is half the job; the other half is knowing what to assert. Calendar code has a small set of properties that hold for almost any implementation and catch most of its bugs.",[10,493,494,497],{},[447,495,496],{},"Monotonicity."," \"Next billing date\" is always later than the input; \"start of week\" is never later than the input; adding a positive duration never moves backwards. These are cheap to state and fail on off-by-one and DST errors immediately.",[10,499,500,503],{},[447,501,502],{},"Coverage without gaps or overlaps."," Splitting a range into periods — days, billing cycles, reporting weeks — must produce contiguous pieces whose union is the whole range. The test above checks the ends and adjacency; together those rule out gaps, overlaps and lost time at DST transitions.",[10,505,506,509],{},[447,507,508],{},"Idempotence."," Normalising a date twice gives the same result as once; truncating to the start of a month twice is the same as once. Violations usually mean the operation depends on the local time of day, which matters exactly when a DST change moves midnight.",[10,511,512,515],{},[447,513,514],{},"Round trips."," Serialise and parse, convert out and back, store and load — the instant must survive, as the section above shows.",[10,517,518],{},"A calendar module with one test of each kind, driven by the constrained strategies here, is covered far better than one with dozens of hand-picked examples, because each property is checked against every boundary the strategy reaches rather than the handful an author thought to write down.",[296,520,522,593],{"className":521},[299],[301,523,309,528,309,531,309,534,309,538,309,543,309,551,309,557,309,560,309,564,309,568,309,571,309,575,309,579,309,583,309,587,309,590],{"viewBox":524,"role":304,"ariaLabelledBy":525,"xmlns":308},"0 0 800 236",[526,527],"cp-t","cp-d",[311,529,530],{"id":526},"Four properties for calendar code",[315,532,533],{"id":527},"Four cards. Monotonicity checks that next or later operations never move backwards. Coverage checks that split periods are contiguous and cover the whole range. Idempotence checks that normalising twice equals once. Round trips check that storing and loading preserves the instant.",[319,535],{"x":321,"y":321,"width":536,"height":537,"rx":324,"fill":325},"800","236",[327,539,542],{"x":540,"y":330,"textAnchor":331,"fontSize":541,"fontWeight":333,"fill":334},"400","15.5","Four properties catch most calendar bugs",[319,544],{"x":545,"y":546,"width":547,"height":548,"rx":351,"fill":549,"stroke":376,"strokeWidth":550},"24","50","370","78","#e6f0ea","2",[327,552,556],{"x":553,"y":554,"fontSize":555,"fontWeight":333,"fill":334},"44","76","12","monotonicity",[327,558,559],{"x":553,"y":345,"fontSize":351,"fill":334},"next_billing_date(d) > d, always",[319,561],{"x":562,"y":546,"width":547,"height":548,"rx":351,"fill":563,"stroke":362,"strokeWidth":550},"406","#f7f0da",[327,565,567],{"x":566,"y":554,"fontSize":555,"fontWeight":333,"fill":334},"426","coverage",[327,569,570],{"x":566,"y":345,"fontSize":351,"fill":334},"periods contiguous, ends match the range",[319,572],{"x":545,"y":573,"width":547,"height":548,"rx":351,"fill":574,"stroke":347,"strokeWidth":550},"138","#fbe9e3",[327,576,578],{"x":553,"y":577,"fontSize":555,"fontWeight":333,"fill":334},"164","idempotence",[327,580,582],{"x":553,"y":581,"fontSize":351,"fill":334},"188","start_of_month(start_of_month(d)) == start_of_month(d)",[319,584],{"x":562,"y":573,"width":547,"height":548,"rx":351,"fill":585,"stroke":334,"strokeWidth":586},"#f4f1de","1.6",[327,588,589],{"x":566,"y":577,"fontSize":555,"fontWeight":333,"fill":334},"round trip",[327,591,592],{"x":566,"y":581,"fontSize":351,"fill":334},"load(store(t)) is the same instant",[410,594,595],{},"Each is one line of assertion; the strategy supplies the hundreds of boundary cases it is checked against.",[22,597,599],{"id":598},"round-tripping-through-utc-and-storage","Round-tripping through UTC and storage",[10,601,602],{},"A particularly productive property for any system that stores times is the round trip: convert to UTC for storage, read it back, convert to the original zone, and compare. It sounds trivial and finds real bugs, because it passes through exactly the code paths — serialisation, database drivers, offset handling — where timezone information gets lost.",[60,604,606],{"className":62,"code":605,"language":64,"meta":65,"style":65},"from hypothesis import given\n\n\n@given(moments)\ndef test_stored_times_round_trip(moment):\n    stored = to_storage(moment)                      # e.g. ISO string in UTC\n    restored = from_storage(stored).astimezone(moment.tzinfo)\n    assert restored == moment                        # the same instant\n",[14,607,608,613,617,621,626,631,636,641],{"__ignoreMap":65},[69,609,610],{"class":71,"line":72},[69,611,612],{},"from hypothesis import given\n",[69,614,615],{"class":71,"line":78},[69,616,82],{"emptyLinePlaceholder":81},[69,618,619],{"class":71,"line":85},[69,620,82],{"emptyLinePlaceholder":81},[69,622,623],{"class":71,"line":91},[69,624,625],{},"@given(moments)\n",[69,627,628],{"class":71,"line":96},[69,629,630],{},"def test_stored_times_round_trip(moment):\n",[69,632,633],{"class":71,"line":102},[69,634,635],{},"    stored = to_storage(moment)                      # e.g. ISO string in UTC\n",[69,637,638],{"class":71,"line":108},[69,639,640],{},"    restored = from_storage(stored).astimezone(moment.tzinfo)\n",[69,642,643],{"class":71,"line":113},[69,644,645],{},"    assert restored == moment                        # the same instant\n",[10,647,648],{},"The equality compares instants, not wall-clock representations, which is correct: a time stored at 01:30 during a fold must come back as the same instant, not merely the same local digits. The failures this finds are almost always one of three: an offset truncated to whole hours (breaking India, Nepal and parts of Australia), microseconds dropped by a serialiser, or the fold flag lost so that the second 01:30 comes back as the first. Each shrinks to a single moment in a single zone, which makes the cause immediately visible.",[296,650,652,748],{"className":651},[299],[301,653,309,657,309,660,309,663,309,679,309,681,309,684,309,690,309,695,309,699,309,703,309,707,309,712,309,716,309,719,309,723,309,727,309,730,309,734,309,739,309,743],{"viewBox":524,"role":304,"ariaLabelledBy":654,"xmlns":308},[655,656],"rt-t","rt-d",[311,658,659],{"id":655},"Round-trip property for stored times",[315,661,662],{"id":656},"A generated aware moment is converted to UTC and serialised for storage, then read back and converted to the original zone. The result must be the same instant. Typical failures are offsets truncated to whole hours, microseconds dropped, and the fold flag lost during a daylight-saving repeat.",[664,665,666,667,309],"defs",{},"\n    ",[668,669,675],"marker",{"id":670,"viewBox":671,"refX":402,"refY":672,"markerWidth":673,"markerHeight":673,"orient":674},"rt-a","0 0 10 10","5","7","auto-start-reverse",[676,677],"path",{"d":678,"fill":334},"M0 0 L10 5 L0 10 z",[319,680],{"x":321,"y":321,"width":536,"height":537,"rx":324,"fill":325},[327,682,683],{"x":540,"y":330,"textAnchor":331,"fontSize":541,"fontWeight":333,"fill":334},"Out to storage and back: the same instant",[319,685],{"x":686,"y":687,"width":688,"height":687,"rx":689,"fill":563,"stroke":362,"strokeWidth":550},"26","60","170","10",[327,691,694],{"x":692,"y":693,"textAnchor":331,"fontSize":407,"fontWeight":333,"fill":334},"111","86","moment",[327,696,698],{"x":692,"y":697,"textAnchor":331,"fontSize":369,"fill":334},"104","Asia\u002FKathmandu",[71,700],{"x1":400,"y1":701,"x2":537,"y2":701,"stroke":334,"strokeWidth":586,"markerEnd":702},"90","url(#rt-a)",[319,704],{"x":705,"y":687,"width":706,"height":687,"rx":689,"fill":585,"stroke":334,"strokeWidth":586},"242","150",[327,708,711],{"x":709,"y":710,"textAnchor":331,"fontSize":407,"fill":334},"317","95","to UTC, store",[71,713],{"x1":714,"y1":701,"x2":715,"y2":701,"stroke":334,"strokeWidth":586,"markerEnd":702},"396","432",[319,717],{"x":718,"y":687,"width":706,"height":687,"rx":689,"fill":585,"stroke":334,"strokeWidth":586},"438",[327,720,722],{"x":721,"y":710,"textAnchor":331,"fontSize":407,"fill":334},"513","read, convert back",[71,724],{"x1":725,"y1":701,"x2":726,"y2":701,"stroke":334,"strokeWidth":586,"markerEnd":702},"592","628",[319,728],{"x":729,"y":687,"width":339,"height":687,"rx":689,"fill":549,"stroke":376,"strokeWidth":550},"634",[327,731,733],{"x":732,"y":710,"textAnchor":331,"fontSize":407,"fontWeight":333,"fill":334},"704","== moment?",[319,735],{"x":686,"y":736,"width":737,"height":738,"rx":351,"fill":574,"stroke":347,"strokeWidth":550},"146","748","70",[327,740,742],{"x":741,"y":688,"fontSize":555,"fontWeight":333,"fill":334},"46","typical shrunk failures",[327,744,747],{"x":741,"y":745,"fontSize":351,"fill":746},"194","#8f3d22","+05:45 truncated to +05:00 · microseconds dropped · fold lost in the repeated hour",[410,749,750],{},"Each failure shrinks to one moment in one zone — usually a zone with a non-hour offset, which hand-written tests almost never use.",[22,752,754],{"id":753},"pinning-the-boundaries-that-were-found","Pinning the boundaries that were found",[10,756,757,758,761],{},"When a property finds a calendar bug, the counterexample is precious: a specific instant in a specific zone that broke the code. The fix should come with that instant pinned as an explicit ",[14,759,760],{},"@example",", so it runs on every future execution regardless of what the generator draws.",[60,763,765],{"className":62,"code":764,"language":64,"meta":65,"style":65},"import datetime as dt\nfrom zoneinfo import ZoneInfo\n\nfrom hypothesis import example, given\n\n\n@given(moments)\n@example(dt.datetime(2026, 10, 25, 1, 30, fold=1, tzinfo=ZoneInfo(\"Europe\u002FLondon\")))\n@example(dt.datetime(2026, 3, 29, 1, 30, tzinfo=ZoneInfo(\"Europe\u002FLondon\")))   # gap\ndef test_stored_times_round_trip(moment):\n    ...\n",[14,766,767,771,776,780,785,789,793,797,802,807,811],{"__ignoreMap":65},[69,768,769],{"class":71,"line":72},[69,770,75],{},[69,772,773],{"class":71,"line":78},[69,774,775],{},"from zoneinfo import ZoneInfo\n",[69,777,778],{"class":71,"line":85},[69,779,82],{"emptyLinePlaceholder":81},[69,781,782],{"class":71,"line":91},[69,783,784],{},"from hypothesis import example, given\n",[69,786,787],{"class":71,"line":96},[69,788,82],{"emptyLinePlaceholder":81},[69,790,791],{"class":71,"line":102},[69,792,82],{"emptyLinePlaceholder":81},[69,794,795],{"class":71,"line":108},[69,796,625],{},[69,798,799],{"class":71,"line":113},[69,800,801],{},"@example(dt.datetime(2026, 10, 25, 1, 30, fold=1, tzinfo=ZoneInfo(\"Europe\u002FLondon\")))\n",[69,803,804],{"class":71,"line":119},[69,805,806],{},"@example(dt.datetime(2026, 3, 29, 1, 30, tzinfo=ZoneInfo(\"Europe\u002FLondon\")))   # gap\n",[69,808,809],{"class":71,"line":125},[69,810,630],{},[69,812,813],{"class":71,"line":130},[69,814,815],{},"    ...\n",[10,817,818],{},"Two such examples — the repeated hour and the missing hour in the zone the business actually operates in — are worth adding to every timezone-sensitive property even before any bug is found. The generator will reach similar cases eventually, but these are the two instants most likely to occur in production, and guaranteeing they are checked on every run costs nothing. Over time the list of pinned examples becomes a compact record of the calendar edge cases the system has been bitten by, which is useful documentation in its own right.",[22,820,822],{"id":821},"frequently-asked-questions","Frequently Asked Questions",[10,824,825,828,829,831],{},[447,826,827],{},"Should strategies generate naive or aware datetimes?","\nWhichever the code under test accepts, and if it accepts both, generate both in separate properties. Most application code should handle only aware datetimes, and a strategy that passes ",[14,830,425],{}," exercises exactly that, including zones with unusual offsets and historical changes.",[10,833,834,837,838,840,841,844,845,848,849,851],{},[447,835,836],{},"How do I make sure DST edge cases are generated?","\nUse ",[14,839,468],{}," for real IANA zones and leave ",[14,842,843],{},"allow_imaginary"," at its default of ",[14,846,847],{},"True",". Hypothesis then includes local times that fall in DST gaps and folds. If the code must reject non-existent times, keep them; if it cannot meaningfully handle them, set ",[14,850,457],{}," deliberately and document why.",[10,853,854,857],{},[447,855,856],{},"How do I generate a start and end date where end is after start?","\nGenerate the start, then generate a non-negative timedelta and add it. Filtering two independent dates for ordering discards half of every draw and shrinks poorly, while constructing the range guarantees validity and shrinks to a zero-length range at the earliest bound.",[22,859,861],{"id":860},"related","Related",[27,863,864,870,877,884],{},[30,865,866,869],{},[51,867,868],{"href":53},"Designing Strategies for Domain Data"," — construction over filtering, applied generally.",[30,871,872,876],{},[51,873,875],{"href":874},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Finjecting-a-clock-instead-of-patching-datetime\u002F","Injecting a Clock Instead of Patching datetime"," — feeding generated moments into a fake clock.",[30,878,879,883],{},[51,880,882],{"href":881},"\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002Fround-trip-properties-for-serializers-and-parsers\u002F","Round-Trip Properties for Serializers and Parsers"," — the round-trip pattern in general.",[30,885,886,890],{},[51,887,889],{"href":888},"\u002Fproperty-based-fuzz-testing-strategies\u002Fadvanced-property-based-testing\u002Fwhy-hypothesis-shrinking-stalls-and-how-to-fix-it\u002F","Why Hypothesis Shrinking Stalls and How to Fix It"," — why filtered date pairs shrink badly.",[10,892,893,894],{},"← Back to ",[51,895,868],{"href":53},[897,898,899],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":65,"searchDepth":78,"depth":78,"links":901},[902,903,904,905,906,907,908,909,910],{"id":24,"depth":78,"text":25},{"id":57,"depth":78,"text":58},{"id":415,"depth":78,"text":416},{"id":440,"depth":78,"text":441},{"id":487,"depth":78,"text":488},{"id":598,"depth":78,"text":599},{"id":753,"depth":78,"text":754},{"id":821,"depth":78,"text":822},{"id":860,"depth":78,"text":861},"Generate datetimes that find real calendar bugs: bounded ranges, aware versus naive values, IANA timezones, DST gaps and folds, ordered date ranges and month-end bias.","md",{"slug":914,"type":915,"breadcrumb":916,"datePublished":917,"dateModified":917,"faq":918,"howto":925},"constraining-dates-and-timezones-in-strategies","article","Dates & Timezones","2026-09-18",[919,921,923],{"q":827,"a":920},"Whichever the code under test accepts, and if it accepts both, generate both in separate properties. Most application code should handle only aware datetimes, and a strategy that passes timezones=st.timezones() exercises exactly that, including zones with unusual offsets and historical changes.",{"q":836,"a":922},"Use st.timezones() for real IANA zones and leave allow_imaginary at its default of True. Hypothesis then includes local times that fall in DST gaps and folds. If the code must reject non-existent times, keep them; if it cannot meaningfully handle them, set allow_imaginary=False deliberately and document why.",{"q":856,"a":924},"Generate the start, then generate a non-negative timedelta and add it. Filtering two independent dates for ordering discards half of every draw and shrinks poorly, while constructing the range guarantees validity and shrinks to a zero-length range at the earliest bound.",{"name":926,"description":927,"steps":928},"How to generate dates and times that find calendar bugs","Bound the range to what the system handles, use real timezones, construct ordered ranges, and bias toward boundaries.",[929,932,935,938,941],{"name":930,"text":931},"Bound the range","Set min_value and max_value to the dates the system is designed to handle.",{"name":933,"text":934},"Use real timezones","Pass timezones=st.timezones() so generated values carry IANA zones with DST rules.",{"name":936,"text":937},"Construct ordered ranges","Draw a start and a non-negative delta in a composite rather than filtering two dates.",{"name":939,"text":940},"Bias toward boundaries","Mix in month ends, year ends and DST transition dates with st.one_of.",{"name":942,"text":943},"Decide the imaginary-time policy","Keep allow_imaginary=True unless the code is specified to reject non-existent local times.","\u002Fproperty-based-fuzz-testing-strategies\u002Fdesigning-strategies-for-domain-data\u002Fconstraining-dates-and-timezones-in-strategies",{"title":5,"description":911},"property-based-fuzz-testing-strategies\u002Fdesigning-strategies-for-domain-data\u002Fconstraining-dates-and-timezones-in-strategies\u002Findex","PFznnRrAyU4k0zdvRQojThX4eT2GkOqOWlyDttnqHlg",1789718769087]