-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathty.schema.json
More file actions
3056 lines (3056 loc) · 311 KB
/
Copy pathty.schema.json
File metadata and controls
3056 lines (3056 loc) · 311 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Options",
"type": "object",
"properties": {
"analysis": {
"anyOf": [
{
"$ref": "#/definitions/AnalysisOptions"
},
{
"type": "null"
}
]
},
"build": {
"description": "Configures what `by build` writes, and what a wheel of this project carries.",
"anyOf": [
{
"$ref": "#/definitions/BuildOptions"
},
{
"type": "null"
}
]
},
"editor": {
"description": "Configures the parts of the editor experience that type checking does not decide.",
"anyOf": [
{
"$ref": "#/definitions/EditorOptions"
},
{
"type": "null"
}
]
},
"environment": {
"description": "Configures the type checking environment.",
"anyOf": [
{
"$ref": "#/definitions/EnvironmentOptions"
},
{
"type": "null"
}
]
},
"experimental": {
"anyOf": [
{
"$ref": "#/definitions/ExperimentalOptions"
},
{
"type": "null"
}
]
},
"lowering": {
"description": "Configures how basedpython spells constructs python has no spelling of its own for.",
"anyOf": [
{
"$ref": "#/definitions/LoweringOptions"
},
{
"type": "null"
}
]
},
"overrides": {
"description": "Override configurations for specific file patterns.\n\nEach override specifies include/exclude patterns and rule configurations\nthat apply to matching files. Multiple overrides can match the same file,\nwith later overrides taking precedence.",
"anyOf": [
{
"$ref": "#/definitions/OverridesOptions"
},
{
"type": "null"
}
]
},
"rules": {
"description": "Configures the enabled rules and their severity.\n\nThe keys are either rule names or `all` to set a default severity for all rules.\nSee [the rules documentation](https://ty.dev/rules) for a list of all available rules.\n\nValid severities are:\n\n* `ignore`: Disable the rule.\n* `warn`: Enable the rule and create a warning diagnostic.\n* `error`: Enable the rule and create an error diagnostic.\n\nBy default, ty exits with code 1 if it emits any warning or error diagnostics.\nSet `terminal.error-on-warning` to `false` to exit with code 0 if all diagnostics have `warning` severity.",
"anyOf": [
{
"$ref": "#/definitions/Rules"
},
{
"type": "null"
}
]
},
"run": {
"description": "Configures how `by run` executes the project.",
"anyOf": [
{
"$ref": "#/definitions/RunOptions"
},
{
"type": "null"
}
]
},
"src": {
"anyOf": [
{
"$ref": "#/definitions/SrcOptions"
},
{
"type": "null"
}
]
},
"terminal": {
"anyOf": [
{
"$ref": "#/definitions/TerminalOptions"
},
{
"type": "null"
}
]
},
"type-checking-preset": {
"description": "The defaults that `rules` and `analysis` start from.\n\nA preset decides which diagnostics exist and which of them are enabled, and it supplies\nthe default for every `analysis` option. Both tables are still read, and both still win\nover the preset, so a preset is a starting point rather than a straitjacket.\n\n* `strict`: every diagnostic is enabled, and every analysis option that buys soundness\n is on. This is the default.\n* `ty-compatible`: the defaults of [ty](https://github.com/astral-sh/ty), which\n basedpython is built on. basedpython's own diagnostics and analysis options are off,\n so that a project reports what ty itself would report. A diagnostic that doesn't exist\n in ty can't be enabled under this preset, not even with `rules = { all = \"error\" }`.\n\nDefaults to `strict`.",
"anyOf": [
{
"$ref": "#/definitions/TypeCheckingPreset"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"definitions": {
"AnalysisOptions": {
"type": "object",
"properties": {
"allowed-unresolved-imports": {
"description": "A list of module glob patterns for which `unresolved-import` diagnostics should be suppressed.\n\nDetails on supported glob patterns:\n- `*` matches zero or more characters except `.`. For example, `foo.*` matches `foo.bar` but\n not `foo.bar.baz`; `foo*` matches `foo` and `foobar` but not `foo.bar` or `barfoo`; and `*foo`\n matches `foo` and `barfoo` but not `foo.bar` or `foobar`.\n- `**` matches any number of module components (e.g., `foo.**` matches `foo`, `foo.bar`, etc.)\n- Prefix a pattern with `!` to exclude matching modules\n\nWhen multiple patterns match, later entries take precedence.\n\nGlob patterns can be used in combinations with each other. For example, to suppress errors for\nany module where the first component contains the substring `test`, use `*test*.**`.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/string"
}
},
"bivariant-private-attributes": {
"description": "Whether a private attribute leaves an inferred type parameter bivariant. This is a\nbasedpython feature.\n\nA private (single-underscore or name-mangled) member is invisible to external observers, so\nit cannot be used to distinguish two specializations of its class, and therefore cannot\nconstrain the class's variance:\n\n```python\nclass A[T]:\n _t: T\n```\n\nWith this option enabled, `T` is inferred bivariant: nothing on `A`'s public surface\nmentions `T`, so `A[int]` and `A[object]` are mutually assignable. As soon as a public\nmember mentions `T`, that member drives the inference as usual.\n\nWhen set to `false`, a private attribute is instead treated as immutable-but-readable,\nwhich constrains the type parameter to covariance.\n\nDefaults to `true`, and to `false` under the `ty-compatible` type checking preset.",
"type": [
"boolean",
"null"
]
},
"block-scoped-declarations": {
"description": "Whether a `let` or `var` declaration written inside a block binds its name for\nthat block only. This is a basedpython feature.\n\nPython has no block scopes: a name bound anywhere in a function is a local of\nthat whole function, and the python a `.by` file lowers to keeps it that way. So\nthis is a rule the checker enforces rather than something the emitted code does:\n\n```by\nif flag:\n let a = 1\n\nprint(a) # error: `a` is not in scope here\n```\n\nOnly the binding keyword scopes a name to its block. A plain `a = 1` binds for\nthe whole enclosing function or module, as it does in python.\n\nDefaults to `true`, and to `false` under the `ty-compatible` type checking preset.",
"type": [
"boolean",
"null"
]
},
"dependency-groups": {
"description": "The requirement groups the matching files may import from.\n\n`project` names `[project].dependencies`, an extra or a PEP 735 dependency group\nis named by its own name, and `*` names every group.\n\nWhen this is unset, a file may import from every group unless it is part of what\nthe project ships — the modules named by `shipped-modules` — in which case it may\nimport only `project` and the extras. Nothing the project ships can import a\ndependency group, because nothing installs one alongside the project.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/string"
}
},
"disable-fluid-specializations": {
"description": "Whether to disable \"fluid specializations\", a basedpython feature that widens the\ninferred generic specialization of an unannotated binding flow-sensitively based on\nits later uses in the same scope.\n\nWhen set to `true`, each unannotated binding keeps the specialization it was inferred\nwith at its creation site; later uses no longer widen or lock it.\n\nDefaults to `false`, and to `true` under the `ty-compatible` type checking preset.",
"type": [
"boolean",
"null"
]
},
"exported-dependencies": {
"description": "The dependencies this project hands to its own users.\n\nA library whose interface is partly made of another distribution's types — one that\nreturns numpy arrays, or takes a pydantic model — can say so, and then a project\nthat depends on this one may import those distributions without declaring them\nitself.\n\nOnly what the project already depends on can be exported, and the claim only\ntravels one link: exporting a distribution does not export whatever *it* depends\non, unless that distribution exports it in turn.\n\nThis is written into the `by.typed` marker when the project is built, because that\nis what its users have — a `pyproject.toml` is not installed with the package.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/string"
}
},
"implicit-object-repr-exempt-types": {
"description": "A list of classes never reported as an\n[`implicit-object-repr`](rules.md#implicit-object-repr).\n\nA class deriving from one of these is exempt too, so listing a base opts out a whole\nhierarchy.\n\nEntries are qualified class names (`decimal.Decimal`). A class in `builtins` may also be\nspelled bare (`int`).",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/string"
}
},
"implicit-object-repr-report-types": {
"description": "A list of classes whose stub is taken at its word when looking for an\n[`implicit-object-repr`](rules.md#implicit-object-repr).\n\nA stub normally settles nothing, because it omits `__str__` and `__repr__` whether or not\nthe runtime class has them — `int` declares neither and still prints as a number. For a\nclass listed here the omission counts as real, the same way it would for a class written\nin source, so a value of that class is reported unless the stub does declare one.\n\nDefaults to the two whose bare repr is seen most often: `types.FunctionType`, which prints\n`<function f at 0x...>`, and `builtins.type`, which prints `<class 'C'>`.\n\nEntries are qualified class names (`decimal.Decimal`). A class in `builtins` may also be\nspelled bare (`int`).",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/string"
}
},
"infer-unannotated-signatures": {
"description": "Whether a function with no annotations is given the signature its body determines. This is\na basedpython feature.\n\nPython's gradual guarantee makes an unannotated `def` say nothing: its parameters accept\nanything and it returns `Unknown`. That is the largest remaining source of `Unknown` in an\notherwise typed project, and it silently swallows real mistakes. With this enabled, the\nmissing half of the signature is recovered from what the function itself already determines:\n\n- **Each unannotated parameter** opens an anonymous type parameter named after it — the same\n hole `some` spells by hand — bounded by everything the function requires of it: the\n promoted type of its default, the members its body reads and calls, the parameters it is\n forwarded into, and any `assert` at the top of the body. Naming the hole is what keeps\n what a call passes in connected to what it gets back, so `def ident(x): return x` is\n inferred as the identity function.\n- **A missing return type** is the union of what the body returns, plus `None` when control\n can also fall off the end. An empty body returns `None`, a body that always raises returns\n `Never`, and a generator returns a generator.\n\nNothing is invented from a use this analysis cannot read, so such a parameter stays gradual\nand its body keeps type-checking exactly as it did. An explicit annotation always wins, and\nso does anything an overload group or an overridden base method already supplies.\n\nDefaults to `true`, and to `false` under the `ty-compatible` type checking preset.",
"type": [
"boolean",
"null"
]
},
"overlapping-condition-assume-truthy-instances": {
"description": "Whether an instance with no `__bool__` and no `__len__` counts as always truthy when\nlooking for an [`overlapping-condition`](rules.md#overlapping-condition).\n\nSuch an instance is only *ambiguously* truthy — a subclass may define `__bool__` — so by\ndefault it is a falsy member of `if not x` just as `None` is. Enabling this assumes the\nclass means what it looks like it means, which drops the reports for the very common\n`if not x` over an optional instance.\n\nDefaults to `false`.",
"type": [
"boolean",
"null"
]
},
"overlapping-condition-exempt-types": {
"description": "A list of classes whose values do not count as a distinct member of an\n[`overlapping-condition`](rules.md#overlapping-condition).\n\n`if not x` over an `int | None` selects both a falsy `int` and `None`, and is reported\nbecause the branch cannot tell them apart. Listing `int` here says that conflating a falsy\n`int` with anything else is fine, so only `None` is left and the condition is accepted.\n\nEntries are qualified class names (`decimal.Decimal`). A class in `builtins` may also be\nspelled bare (`int`), and `None` stands for the type of `None`.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/string"
}
},
"precise-unsolved-typevars": {
"description": "Whether a type variable that a call leaves unsolved is solved to `Never`. This is a\nbasedpython feature.\n\nA call can leave a type variable entirely unsolved, because no argument mentions it:\n\n```python\ndef f[T]() -> T: ...\n\na = f()\n```\n\n`Never` is the precise answer here: no value ever reaches that position, so nothing the\ncall returns can be observed at type `T`. When set to `false`, the type variable falls back\nto the gradual `Unknown` instead, which silences any error that would follow from the call\nsite.\n\nThis applies where the type variable is an output. Where it is instead written through or\npassed back in — the element of an invariant `list[T]`, the parameter of a returned\n`Callable[[T], R]` — `Never` would say that nothing can ever be put there, so an invariant\nor contravariant occurrence keeps the gradual `Unknown`.\n\nA PEP 696 default (`def f[T = str]()`) always takes priority, and a `ParamSpec`,\n`TypeVarTuple` or keyword-variadic pack is unaffected because `Never` is not a valid\nsolution for one.\n\nDefaults to `true`, and to `false` under the `ty-compatible` type checking preset.",
"type": [
"boolean",
"null"
]
},
"replace-imports-with-any": {
"description": "A list of module glob patterns whose imports should be replaced with `typing.Any`.\n\nUnlike `allowed-unresolved-imports`, this setting replaces the module's type information\nwith `typing.Any` even if the module can be resolved. Import diagnostics are\nunconditionally suppressed for matching modules.\n\n- Prefix a pattern with `!` to exclude matching modules\n\nWhen multiple patterns match, later entries take precedence.\n\nGlob patterns can be used in combinations with each other. For example, to suppress errors for\nany module where the first component contains the substring `test`, use `*test*.**`.\n\nWhen multiple patterns match, later entries take precedence.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/string"
}
},
"respect-type-ignore-comments": {
"description": "Whether ty should respect `type: ignore` comments.\n\nWhen set to `false`, `type: ignore` comments are treated like any other normal\ncomment and can't be used to suppress ty errors (you have to use `ty: ignore` instead).\n\nSetting this option can be useful when using ty alongside other type checkers or when\nyou prefer using `ty: ignore` over `type: ignore`.\n\nDefaults to `true`.",
"type": [
"boolean",
"null"
]
},
"shipped-modules": {
"description": "The top-level modules the project ships.\n\nDefaults to the module named after `[project].name`: a project named `my-lib`\nships `my_lib`. Only a project that ships several unrelated modules, or one whose\nmodule is not named after it, needs to say.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/string"
}
},
"sound-types": {
"description": "Whether to infer sound (non-gradual) types wherever a precise type is available. This is a\nbasedpython feature.\n\nPython's gradual guarantee requires a type checker to fall back to a gradual type whenever\nan annotation is missing, even when a precise type could be inferred. In a fully typed\nproject that is pure boilerplate: it forces an annotation to be written for something the\nchecker already knows. When set to `true`, this option deliberately breaks the gradual\nguarantee and uses the precise type instead. It affects:\n\n- **Unannotated parameters**: each one opens an anonymous type parameter named after it,\n bounded by everything the function requires of it — the promoted type of its default, the\n members its body reads and calls, the parameters it is forwarded into, and any `assert` at\n the top of the body. So `def f(a=1)` rejects a `str` at a call site, and\n `def ident(x): return x` is inferred as the identity function. A lambda parameter with a\n default takes that default's promoted type directly.\n- **Unannotated return types**: the union of what the body returns, plus `None` when control\n can fall off the end. An empty body returns `None` and a body that always raises returns\n `Never`; a generator returns a generator.\n- **Unannotated methods that override a base method**: the parameter and return types are\n inherited from the overridden method, including from `Protocol` members and\n `abstractmethod` declarations.\n- **Bare `ClassVar` annotations**: `x: ClassVar = 1` declares `int` rather than the union of\n `Unknown` and the inferred type.\n- **Empty collection literals**: `[]` has element type `Never`, so passing one to a generic\n call solves from it precisely instead of leaking `Unknown`.\n\nAn explicit annotation always takes priority over any of the above.\n\nDefaults to `true`, and to `false` under the `ty-compatible` type checking preset.",
"type": [
"boolean",
"null"
]
},
"strict-equality-semantics": {
"description": "Configure ty's behavior regarding type inference and narrowing of equality\nchecks.\n\nDefaults to `true`, and to `false` under the `ty-compatible` type checking preset.\n\nWith this option disabled, ty makes various assumptions about equality checks that\nmatch the intuitions of most Python programmers, but may not be fully sound in all\nsituations. Leaving it enabled makes ty conservative about those assumptions, making it\nless likely to infer `Literal[True]` or `Literal[False]` as the result of an\nequality check. This has various effects on type checking, including fewer type\nnarrowing opportunities and more conservative assumptions regarding control flow.\n\nOne such unsound assumption is narrowing an object `x` of type `str` to `Literal[\"a\"]`\nafter an `if x == \"a\"` check. This is unsound because a subclass of `str` with value\n`\"a\"` will (by default) compare equal to `\"a\"`, but will not be of type `Literal[\"a\"]`:\n\n```pycon\n>>> # `Literal[\"a\"]` can only be inhabited by instances of exactly `str`, not\n>>> # subclasses, but str subclasses compare equal by default:\n>>> class StringSubclass(str): ...\n...\n>>> StringSubclass(\"a\") == \"a\"\nTrue\n>>>\n>>> # This also applies to `StrEnum`s:\n>>> from enum import StrEnum\n>>> class MyEnum(StrEnum):\n... A = \"a\"\n...\n>>> MyEnum.A == \"a\"\nTrue\n```\n\nThis option prevents the unsound narrowing of `x` to `Literal[\"a\"]`, and instead keeps\nit as `str`:\n\n```python\nfrom typing import Literal\n\ndef parse(value: str) -> Literal[\"a\"] | None:\n # with `strict-equality-semantics` enabled, no narrowing will occur here,\n # and an error will be emitted on the `return` statement.\n if value == \"a\":\n return value\n return None\n```\n\nAnother assumption ty makes by default is that subclasses will never override `__eq__` or\n`__ne__`. This allows ty to narrow the following union based on an equality check, despite\nthe fact that an instance of a subclass of `Foo` could compare equal to `None`, and it's\nperfectly valid to pass an instance of a subclass into the `x` parameter of this function:\n\n```python\ndef narrow(x: Foo | None, other: Foo) -> None:\n if x == other:\n # with this option enabled, `x` still has type `Foo | None` here,\n # since it is legal to subclass `Foo` and override its `__eq__` method.\n reveal_type(x)\n```\n\nMany operations in Python implicitly call `__eq__` under the hood, and this option\nimpacts those too. For example, it also impacts narrowing from `in` checks, and narrowing\nin `match` statements that use value patterns:\n\n```python\ndef narrow_in(x: Foo | None, other: list[Foo]) -> None:\n if x in other:\n # with this option enabled, `x` still has type `Foo | None` here,\n # since the `in` operator implicitly calls `__eq__` on each element of `other`.\n reveal_type(x)\n\n\ndef narrow_match(x: str) -> None:\n match x:\n case \"a\":\n # with this option enabled, `x` still has type `str` here,\n # since this `case` branch will be taken by any object that compares\n # equal to `\"a\"`, including subclasses of `str`.\n reveal_type(x)\n```",
"type": [
"boolean",
"null"
]
},
"strict-float": {
"description": "Whether `float` and `complex` annotations mean *only* themselves. This is a\nbasedpython feature.\n\nThe typing spec's special case says an `int` is acceptable wherever a `float` is\nasked for, so `x: float` really declares `int | float`. A `.by` file opts out of\nthat already; this makes the same model available to a `.py` one, per module.\n\nIt is not only a checking question. The wider annotation is why a `.py`\n`list[float]` cannot be laid out as an unboxed buffer and a `.py` class cannot\nhave `double` fields, so `by compile` reads this to choose a representation.\n\nDefaults to `false`.",
"type": [
"boolean",
"null"
]
},
"strict-generic-narrowing": {
"description": "Whether ty should use strict narrowing for unspecialized generic classes in\n`isinstance()` and `issubclass()` checks, as well as `match` class patterns.\n\nWhen enabled, ty narrows to the top materialization of the class. For example,\n`isinstance(value, list)` narrows a value of type `object` to `Top[list[Unknown]]`,\nrepresenting the (infinite) union of all possible `list` specializations. Iterating\nover the list would yield values of type `object`.\n\nWhen disabled, ty uses gradual generic narrowing, preserving compatible type\narguments from the original type where possible. For example,\n`isinstance(value, list)` narrows a value of type `Sequence[int]` to `list[int]`.\nIf no specialization is available, the same check narrows a value of type `object`\nto `list[Unknown]`; items of any type can then be appended to the list. Class\npatterns such as `case list():` follow the same behavior.\n\nDefaults to `false`.",
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false
},
"Array_of_string": {
"type": "array",
"items": {
"$ref": "#/definitions/string"
}
},
"BuildOptions": {
"type": "object",
"properties": {
"exclude": {
"description": "Files to keep out of the build output.\n\nThe syntax is the same as `src.exclude`, and paths are anchored to the\nproject root. Excluding a `.by` file keeps its transpiled output out of\nthe build as well.",
"anyOf": [
{
"$ref": "#/definitions/Array_of_string"
},
{
"type": "null"
}
]
},
"include": {
"description": "Files to carry into the build output verbatim, in addition to the ones\nthat are there by default.\n\n`by build` mirrors the whole module tree: a `.by` file is transpiled, and\nevery other file — a hand-written `.py`, a `py.typed` marker, a template,\na data file — is copied to the same place in the output. `include` is for\nthe files that sit *outside* a module root and still belong in the build,\nsuch as a data directory next to `src`.\n\nThe syntax is the same as `src.include`, and paths are anchored to the\nproject root. `exclude` takes precedence over `include`.",
"anyOf": [
{
"$ref": "#/definitions/Array_of_string"
},
{
"type": "null"
}
]
},
"sources": {
"description": "Whether the build output carries the `.by` sources alongside the python\nthey were transpiled into, with a `by.typed` marker naming them as the\nauthoritative surface.\n\nThis is what lets one basedpython project depend on another: a downstream\npython project reads the transpiled `.py` and is served perfectly, while a\ndownstream basedpython project reads the `.by` and keeps the declarations\nthat have no python spelling — `extension` blocks, `raises` clauses,\nread-only `let`, sum types.\n\nEnabled by default. Turn it off to ship python only.",
"type": [
"boolean",
"null"
]
},
"version-from": {
"description": "The module to read `__version__` from, when `[project]` declares\n`dynamic = [\"version\"]`.\n\nThis is read when a wheel or a source distribution is built, not by the\nchecker: a version has to be settled before the packaging backend sees the\nproject, and the place it lives is a `.by` module that backend cannot\nread.\n\nThe value is a path relative to the project root.",
"anyOf": [
{
"$ref": "#/definitions/string"
},
{
"type": "null"
}
]
},
"wheel-versions": {
"description": "The python versions to build a wheel for, one wheel each.\n\n`by build --wheels` builds one wheel per version listed and tags each for\nthe python it was lowered to, so an installer hands every interpreter the\nbest wheel it can use. A python with no wheel of its own takes the newest\none below it.\n\nDefaults to every version from the one the project targets up to the\nnewest this release knows about — which is what `requires-python` already\nsays the project supports, so most projects need not set this. List them\nexplicitly to ship fewer.",
"anyOf": [
{
"$ref": "#/definitions/Array_of_string"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
"CommonAliases": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"EditorOptions": {
"type": "object",
"properties": {
"common-aliases": {
"description": "The modules a name is a common alias of, keyed by the alias.\n\nA file that writes `np.` before importing anything almost always means numpy, because `np`\nis what numpy is conventionally imported as. The editor completes such a name as the module\nit names, and accepting one of those completions writes the `import numpy as np` that makes\nthe name real.\n\nThis adds aliases of your own to the ones ty already knows; an entry whose alias ty knows\nreplaces it. An alias for a module the project does not have is never offered, so an entry\nfor a module nobody installed costs nothing.\n\nDefaults to `{}`, which leaves ty's own aliases as they are.",
"anyOf": [
{
"$ref": "#/definitions/CommonAliases"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
"EnvironmentOptions": {
"type": "object",
"properties": {
"extra-paths": {
"description": "User-provided paths that should take first priority in module resolution.\n\nThis is an advanced option that should usually only be used for first-party or third-party\nmodules that are not installed into your Python environment in a conventional way.\nUse the `python` option to specify the location of your Python environment.\n\nThis option is similar to mypy's `MYPYPATH` environment variable and pyright's `stubPath`\nconfiguration setting.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/RelativePathBuf"
}
},
"python": {
"description": "Path to your project's Python environment or interpreter.\n\nty uses the `site-packages` directory of your project's Python environment\nto resolve third-party (and, in some cases, first-party) imports in your code.\n\nThis can be a path to:\n\n- A Python interpreter, e.g. `.venv/bin/python3`\n- A virtual environment directory, e.g. `.venv`\n- A system Python [`sys.prefix`] directory, e.g. `/usr`\n\nIf you're using a project management tool such as uv, you should not generally need to\nspecify this option, as commands such as `uv run` will set the `VIRTUAL_ENV` environment\nvariable to point to your project's virtual environment. ty can also infer the location of\nyour environment from an activated Conda environment, and will look for a `.venv` directory\nin the project root if none of the above apply. Failing that, ty will look for a `python3`\nor `python` binary available in `PATH`.\n\nScripts with inline metadata use their own Python environment. They can use an explicitly\nconfigured environment, an activated environment, or an environment selected by the editor.\nUnlike projects, they do not automatically use a `.venv` directory.\n\n[`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix",
"anyOf": [
{
"$ref": "#/definitions/RelativePathBuf"
},
{
"type": "null"
}
]
},
"python-platform": {
"description": "Specifies the target platform that will be used to analyze the source code.\nIf specified, ty will understand conditions based on comparisons with `sys.platform`, such\nas are commonly found in typeshed to reflect the differing contents of the standard library across platforms.\nIf `all` is specified, ty will assume that the source code can run on any platform.\n\nIf no platform is specified, ty will use the current platform:\n- `win32` for Windows\n- `darwin` for macOS\n- `android` for Android\n- `ios` for iOS\n- `linux` for everything else",
"anyOf": [
{
"$ref": "#/definitions/PythonPlatform"
},
{
"type": "null"
}
]
},
"python-version": {
"description": "Specifies the version of Python that will be used to analyze the source code.\nThe version should be specified as a string in the format `M.m` where `M` is the major version\nand `m` is the minor (e.g. `\"3.7\"` or `\"3.12\"`).\nIf a version is provided, ty will generate errors if the source code makes use of language features\nthat are not supported in that version.\n\nty officially supports type checking code that targets Python 3.10 and later. Python 3.7\nthrough 3.9 can still be selected, but ty may produce false positives or false negatives for\nstandard-library APIs because its bundled stubs do not fully describe those versions.\n\nIf a version is not specified, ty will try the following techniques in order of preference\nto determine a value:\n1. Check for the `project.requires-python` setting in a `pyproject.toml` file\n and use the minimum version from the specified range\n2. Check for an activated or configured Python environment\n and attempt to infer the Python version of that environment\n3. Fall back to the default value (see below)\n\nScripts with inline metadata use their `requires-python` field instead of\n`project.requires-python`. They do not inherit the Python version of the enclosing project.\n\nFor some language features, ty can also understand conditionals based on comparisons\nwith `sys.version_info`. These are commonly found in typeshed, for example,\nto reflect the differing contents of the standard library across Python versions.",
"anyOf": [
{
"$ref": "#/definitions/SupportedPythonVersion"
},
{
"type": "null"
}
]
},
"root": {
"description": "The root paths of the project, used for finding first-party modules.\n\nAccepts a list of directory paths searched in priority order (first has highest priority).\n\nIf left unspecified, ty will try to detect common project layouts and initialize `root` accordingly.\nThe project root (`.`) is always included. Additionally, the following directories are included\nif they exist and are not packages (i.e. they do not contain `__init__.py` or `__init__.pyi` files):\n\n* `./src`\n* `./<project-name>` (if a `./<project-name>/<project-name>` directory exists)\n* `./python`\n\nScripts with inline metadata have no first-party roots by default because they are\nsingle-file programs. Set `root = [\".\"]` to allow importing local modules.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/RelativePathBuf"
}
},
"typeshed": {
"description": "Optional path to a \"typeshed\" directory on disk for us to use for standard-library types.\nIf this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,\nbundled as a zip file in the binary",
"anyOf": [
{
"$ref": "#/definitions/RelativePathBuf"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
"ExperimentalOptions": {
"description": "Features that are still being designed, and are off unless the project asks\nfor them by name.\n\nAn experimental feature may change or be withdrawn without the deprecation\nperiod a stable one gets. Opting in says you would rather have it than that\nguarantee.",
"type": "object",
"properties": {
"build-stamps": {
"description": "Whether a `build:` block declares build stamps.\n\n`build:` declares the values a build settles when it produces the artifact\n— the commit it was built from, the time it was built at — and each is read\nas `build.NAME` at the type it declares. With this off the block still\nparses and still lowers, so a program that reads a stamp keeps working, but\nwriting one is reported: nothing settles a stamp the project has not asked\nfor, so it would silently stand for its default, or for nothing.",
"type": [
"boolean",
"null"
]
},
"module-api": {
"description": "Whether an `implements` declaration is enforced.\n\n`implements Backend` obliges the module that writes it to answer the\nprotocol, and a `for` clause in a package's `__init__` imposes the same\nobligation on the modules its patterns name. With this off the declaration\nstill parses and still lowers, but nothing is checked against it — and a\ndeclaration written anyway is reported, rather than quietly doing nothing.",
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false
},
"FloatLiteralLowering": {
"description": "How a float or complex literal type reaches the transpiled python.",
"oneOf": [
{
"title": "Nominal",
"description": "The type the literal is one of: `1.5` becomes `float`, `2j` becomes `complex`.",
"type": "string",
"const": "nominal"
},
{
"title": "Literal",
"description": "The literal itself, inside `Literal[...]`. Runs, but no checker accepts it.",
"type": "string",
"const": "literal"
}
]
},
"Level": {
"oneOf": [
{
"title": "Ignore",
"description": "The lint is disabled and should not run.",
"type": "string",
"const": "ignore"
},
{
"title": "Warn",
"description": "The lint is enabled and diagnostic should have a warning severity.",
"type": "string",
"const": "warn"
},
{
"title": "Error",
"description": "The lint is enabled and diagnostics have an error severity.",
"type": "string",
"const": "error"
}
]
},
"LoweringOptions": {
"type": "object",
"properties": {
"float-literals": {
"description": "How a float or complex literal type is spelled in the transpiled python.\n\nbasedpython reads `a: 1.5` as a literal type, and python has no spelling for one:\nPEP 586 admits only `None`, `int`, `bool`, `str`, `bytes` and enum members into\n`Literal[...]`.\n\n* `nominal` (the default) writes the type the literal is one of — `a: 1.5` becomes\n `a: float`, `a: 2j` becomes `a: complex`. The precision is lost, and every checker\n that reads the output accepts it.\n* `literal` keeps the literal, writing `a: Literal[1.5]`. The precision survives and\n the output still runs, because `typing` does not check what it is handed — but a\n checker reading it reports the argument as invalid.",
"anyOf": [
{
"$ref": "#/definitions/FloatLiteralLowering"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
"OutputFormat": {
"description": "The diagnostic output format.",
"oneOf": [
{
"description": "The default full mode will print \"pretty\" diagnostics.\n\nThat is, color will be used when printing to a `tty`.\nMoreover, diagnostic messages may include additional\ncontext and annotations on the input to help understand\nthe message.",
"type": "string",
"const": "full"
},
{
"description": "Print diagnostics in a concise mode.\n\nThis will guarantee that each diagnostic is printed on\na single line. Only the most important or primary aspects\nof the diagnostic are included. Contextual information is\ndropped.\n\nThis may use color when printing to a `tty`.",
"type": "string",
"const": "concise"
},
{
"description": "Print diagnostics in the JSON format expected by GitLab [Code Quality] reports.\n\n[Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format",
"type": "string",
"const": "gitlab"
},
{
"description": "Print diagnostics in the format used by [GitHub Actions] workflow error annotations.\n\n[GitHub Actions]: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-an-error-message",
"type": "string",
"const": "github"
},
{
"description": "Print diagnostics as a JUnit-style XML report.",
"type": "string",
"const": "junit"
}
]
},
"OverrideOptions": {
"type": "object",
"properties": {
"analysis": {
"anyOf": [
{
"$ref": "#/definitions/AnalysisOptions"
},
{
"type": "null"
}
]
},
"exclude": {
"description": "A list of file and directory patterns to exclude from this override.\n\nPatterns follow a syntax similar to `.gitignore`.\nExclude patterns take precedence over include patterns within the same override.\n\nIf not specified, defaults to `[]` (excludes no files).",
"anyOf": [
{
"$ref": "#/definitions/Array_of_string"
},
{
"type": "null"
}
]
},
"include": {
"description": "A list of file and directory patterns to include for this override.\n\nThe `include` option follows a similar syntax to `.gitignore` but reversed:\nIncluding a file or directory will make it so that it (and its contents)\nare affected by this override.\n\nIf not specified, defaults to `[\"**\"]` (matches all files).",
"anyOf": [
{
"$ref": "#/definitions/Array_of_string"
},
{
"type": "null"
}
]
},
"rules": {
"description": "Rule overrides for files matching the include/exclude patterns.\n\nThese rules will be merged with the global rules, with override rules\ntaking precedence for matching files. You can set rules to different\nseverity levels or disable them entirely.",
"anyOf": [
{
"$ref": "#/definitions/Rules"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false
},
"OverridesOptions": {
"description": "Configuration override that applies to specific files based on glob patterns.\n\nAn override allows you to apply different rule configurations to specific\nfiles or directories. Multiple overrides can match the same file, with\nlater overrides take precedence. Override rules take precedence over global\nrules for matching files.\n\nFor example, to relax enforcement of rules in test files:\n\n```toml\n[[tool.ty.overrides]]\ninclude = [\"tests/**\", \"**/test_*.py\"]\n\n[tool.ty.overrides.rules]\npossibly-unresolved-reference = \"warn\"\n```\n\nOr, to ignore a rule in generated files but retain enforcement in an important file:\n\n```toml\n[[tool.ty.overrides]]\ninclude = [\"generated/**\"]\nexclude = [\"generated/important.py\"]\n\n[tool.ty.overrides.rules]\npossibly-unresolved-reference = \"ignore\"\n```",
"type": "array",
"items": {
"$ref": "#/definitions/OverrideOptions"
}
},
"PythonPlatform": {
"description": "The target platform to assume when resolving types.\n",
"anyOf": [
{
"type": "string"
},
{
"description": "Do not make any assumptions about the target platform.",
"const": "all"
},
{
"description": "Darwin",
"const": "darwin"
},
{
"description": "Linux",
"const": "linux"
},
{
"description": "Windows",
"const": "win32"
}
]
},
"RelativePathBuf": {
"description": "A possibly relative path in a configuration file.\n\nRelative paths in configuration files or from CLI options\nrequire different anchoring:\n\n* CLI: The path is relative to the current working directory\n* Configuration file: The path is relative to the project's or script's configuration root.",
"allOf": [
{
"$ref": "#/definitions/SystemPathBuf"
}
]
},
"Rules": {
"type": "object",
"properties": {
"abstract-and-final-method": {
"title": "detects methods that are both abstract and final",
"description": "## What it does\n\nChecks for methods decorated with both `@abstractmethod` and `@final`.\n\n## Why is this bad?\n\nAn abstract method must be overridden for a subclass to become concrete, but a final method cannot\nbe overridden. Combining the decorators therefore makes it impossible for a subclass to provide a\nconcrete implementation.\n\n## Example\n\n```python\nfrom abc import ABC, abstractmethod\nfrom typing import final\n\n\nclass Base(ABC):\n @final\n @abstractmethod\n def method(self) -> None: ... # error\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"abstract-method-in-final-class": {
"title": "detects `@final` classes with unimplemented abstract methods",
"description": "## What it does\n\nChecks for `@final` classes that have unimplemented abstract methods.\n\n## Why is this bad?\n\nA class decorated with `@final` cannot be subclassed. If such a class has abstract methods that are\nnot implemented, the class can never be properly instantiated, as the abstract methods can never be\nimplemented (since subclassing is prohibited).\n\nAt runtime, instantiation of classes with unimplemented abstract methods is only prevented for\nclasses that have `ABCMeta` (or a subclass of it) as their metaclass. However, type checkers also\nenforce this for classes that do not use `ABCMeta`, since the intent for the class to be abstract is\nclear from the use of `@abstractmethod`.\n\n## Example\n\n```python\nfrom abc import ABC, abstractmethod\nfrom typing import final\n\n\nclass Base(ABC):\n @abstractmethod\n def method(self) -> int: ...\n\n\n@final\n# `Derived` does not implement `method`\nclass Derived(Base): # error\n pass\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"all": {
"title": "set the default severity level for all rules",
"description": "Configure a default severity level for all rules. Individual rule settings override this default.",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"ambiguous-context-argument": {
"title": "detects calls whose `context` parameter matches several context values",
"description": "## What it does\nChecks for calls where several `context` declarations in the same scope\ncould fill one `context` parameter.\n\n## Why is this bad?\nThe implicit argument is chosen by assignability, not by name. When two\ndeclarations in the winning scope both match, either choice would be\narbitrary — the call must pass the argument explicitly (or the extra\ndeclaration must move to another scope).\n\n## Examples\n```python\ndef f(a: int, context b: str): ...\n\ncontext s1 = \"hello\"\ncontext s2 = \"world\"\nf(1) # error: `s1` and `s2` both match\nf(1, b=s1) # ok — explicit\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"ambiguous-conversion": {
"title": "detects conversion sites served by more than one conversion",
"description": "## What it does\nChecks for conversion sites where more than one conversion applies — two\ndunders, a dunder and an in-scope conformance, or two applicable\n`implementation`s of the same interface and type.\n\n## Why is this bad?\n`__from__` and `__into__` are hand-written bodies that can disagree, so\nwhich one runs must not depend on arbitrary ordering. Remove one of them,\nor write the conversion you want explicitly.\n\n## Example\n\n```by\nclass Celsius:\n def __into__(self) -> Fahrenheit: ...\n\nclass Fahrenheit:\n @classmethod\n def __from__(cls, value: Celsius) -> Self: ...\n\nreport(Celsius()) # error: two conversions apply\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"ambiguous-extension-member": {
"title": "detects attribute accesses supplied by more than one extension",
"description": "## What it does\nChecks for attribute accesses that resolve to a member supplied by more\nthan one applicable basedpython extension.\n\n## Why is this bad?\nWhen two extensions in scope both add the same member to the receiver's\ntype, the access is ambiguous — which implementation runs would depend\non arbitrary ordering. Constrain one of the extensions (or drop the\nimport that brings the second into scope) so exactly one applies.\n\n## Example\n\n```by\nextension list:\n def second(self) -> Element: ...\n\nextension list:\n def second(self) -> Element: ...\n\n[1, 2].second() # error: ambiguous extension member\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"ambiguous-protocol-member": {
"title": "detects protocol classes with ambiguous interfaces",
"description": "## What it does\n\nChecks for protocol classes with members that will lead to ambiguous interfaces.\n\n## Why is this bad?\n\nAssigning to an undeclared variable in a protocol class, or to an undeclared attribute through a\nprotocol method's `self` or `cls` receiver, leads to an ambiguous interface which may lead to the\ntype checker inferring unexpected things. It's recommended to ensure that all members of a protocol\nclass are explicitly declared.\n\n## Examples\n\n```py\nfrom typing import ClassVar, Protocol\n\n\nclass BaseProto(Protocol):\n a: int # fine (explicitly declared as `int`)\n instance_member: str\n class_member: ClassVar[str]\n\n # fine: a method definition using `def` is considered a declaration\n def method_member(self) -> int: ...\n\n def method(self) -> None:\n self.instance_member = \"value\" # fine (declared in the class body)\n self.implicit = \"value\" # error: [ambiguous-protocol-member]\n\n @classmethod\n def class_method(cls) -> None:\n cls.class_member = \"value\" # fine (declared in the class body)\n cls.implicit_class = \"value\" # error: [ambiguous-protocol-member]\n\n # no explicit declaration, leading to ambiguity\n c = \"some variable\" # error\n # no explicit declaration, leading to ambiguity\n b = method_member # error\n\n # This creates implicit assignments of `d` and `e` in the protocol class body.\n # Were they really meant to be considered protocol members?\n # error: \"`d` is not declared as a protocol member\"\n # error: \"`e` is not declared as a protocol member\"\n for d, e in enumerate(range(42)):\n pass\n\n\nclass SubProto(BaseProto, Protocol):\n a = 42 # fine (declared in superclass)\n```",
"default": "warn",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"assert-type-unspellable-subtype": {
"title": "detects failed type assertions",
"description": "## What it does\n\nChecks for `assert_type()` calls where the actual type is an unspellable subtype of the asserted\ntype.\n\n## Why is this bad?\n\n`assert_type()` is intended to ensure that the inferred type of a value is exactly the same as the\nasserted type. But in some situations, ty has nonstandard extensions to the type system that allow\nit to infer more precise types than can be expressed in user annotations. ty emits a different error\ncode to `type-assertion-failure` in these situations so that users can easily differentiate between\nthe two cases.\n\n## Example\n\n```toml\n[environment]\npython-version = \"3.11\"\n```\n\n```python\nfrom typing import assert_type\n\n\ndef _(x: int):\n assert_type(x, int) # fine\n if x:\n # the actual type is `int & ~AlwaysFalsy`,\n # which excludes types like `Literal[0]`\n # error: [assert-type-unspellable-subtype]\n assert_type(x, int)\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"blanket-ignore-comment": {
"title": "detects blanket `ty: ignore` comments",
"description": "## What it does\n\nChecks for `ty: ignore` comments that don't specify which rules to ignore.\n\n## Why is this bad?\n\nA blanket `ty: ignore` comment suppresses every type-checking diagnostic on the applicable line or\nfile. Specifying rule codes documents which diagnostics are expected and prevents the comment from\nsilencing unrelated errors.\n\n## Examples\n\n```py\n# error\nvalue = unknown # ty: ignore\n```\n\nUse instead:\n\n```py\nvalue = unknown # ty: ignore[unresolved-reference]\n```",
"default": "warn",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"bool-as-int": {
"title": "detects a `bool` implicitly used as an `int`",
"description": "## What it does\nChecks for a `bool` value in a position that expects a number, where it is\nadmitted only because `bool` is a subclass of `int`.\n\n## Why is this bad?\nNothing is converted here — `bool` really is a subclass of `int`, and `True`\nand `False` really are `1` and `0`. That is the problem: the value satisfies\nan `int` (or `float`, or `complex`) annotation silently, so a boolean that\nreached a numeric slot by mistake type-checks exactly like one that was meant\nto. Writing `int(...)` says the number is what you meant, and widening the\nannotation to `bool` says the flag is.\n\nThe value has to be a boolean and the target a number for this to fire, so\narithmetic on booleans, a `bool` annotation, and a container of booleans are\nall left alone. Note that `int | bool` is not an escape hatch: a union of a\nclass and its subclass simplifies to the supertype, so that annotation *is*\n`int` and is reported as such.\n\n## Examples\n```python\ndef take(n: int): ...\n\na: int = True # warning: `bool` used as `int`\ntake(True) # warning: `bool` used as `int`\n\na2: int = int(True) # ok — explicit\na3: bool = True # ok\na4 = True + 1 # ok — a boolean used as a boolean\n```",
"default": "warn",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"call-abstract-method": {
"title": "detects calls to abstract methods with trivial bodies on class objects",
"description": "## What it does\n\nChecks for calls to abstract `@classmethod`s or `@staticmethod`s with \"trivial bodies\" when accessed\non the class object itself.\n\n\"Trivial bodies\" are bodies that solely consist of `...`, `pass`, a docstring, and/or\n`raise NotImplementedError`.\n\n## Why is this bad?\n\nAn abstract method with a trivial body has no concrete implementation to execute, so calling such a\nmethod directly on the class will probably not have the desired effect.\n\nIt is also unsound to call these methods directly on the class. Unlike other methods, ty permits\nabstract methods with trivial bodies to have non-`None` return types even though they always return\n`None` at runtime. This is because it is expected that these methods will always be overridden\nrather than being called directly. As a result of this exception to the normal rule, ty may infer an\nincorrect type if one of these methods is called directly, which may then mean that type errors\nelsewhere in your code go undetected by ty.\n\nCalling abstract classmethods or staticmethods via `type[X]` is allowed, since the actual runtime\ntype could be a concrete subclass with an implementation.\n\n## Example\n\n```python\nfrom abc import ABC, abstractmethod\n\n\nclass Foo(ABC):\n @classmethod\n @abstractmethod\n def method(cls) -> int: ...\n\n\n# cannot call abstract classmethod\nFoo.method() # error\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"call-non-callable": {
"title": "detects calls to non-callable objects",
"description": "## What it does\n\nChecks for calls to non-callable objects.\n\n## Why is this bad?\n\nCalling a non-callable object will raise a `TypeError` at runtime.\n\n## Examples\n\n```python\n# TypeError: 'int' object is not callable\n4() # error\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"call-top-callable": {
"title": "detects calls to the top callable type",
"description": "## What it does\n\nChecks for calls to objects typed as `Top[Callable[..., T]]` (the infinite union of all callable\ntypes with return type `T`).\n\n## Why is this bad?\n\nWhen an object is narrowed to `Top[Callable[..., object]]` (e.g., via `callable(x)` or\n`isinstance(x, Callable)`), we know the object is callable, but we don't know its precise signature.\nThis type represents the set of all possible callable types (including, e.g., functions that take no\narguments and functions that require arguments), so no specific set of arguments can be guaranteed\nto be valid.\n\n## Examples\n\n```python\ndef f(x: object):\n if callable(x):\n # We know `x` is callable, but not what arguments it accepts\n x() # error\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"composable-outside-composition": {
"title": "detects a composable or builder called outside a composition",
"description": "## What it does\n\nChecks for a call to a `basedpython_ui` composable (a function decorated `@composable`) or to one\nof the framework's widget builders (`Text`, `Button`, `Column`, …) from somewhere that is not a\ncomposition: a function that is not itself a composable, a handler block, a lambda or a nested\n`def`. A composable's body, the `once` content blocks and `local` blocks written in it, and the\n`root` block of `run_app` / `compose_test` are compositions.\n\n## Why is this bad?\n\nA composable opens a scope in the composition being built and a builder emits into it; neither has\nanything to build into outside of one. The runtime raises `CompositionError` at the call; this\ncheck reports it at the source.\n\n## Examples\n\n```by\nfrom basedpython_ui import composable, run_app, Button, Text\n\n@composable\ndef Counter(): ...\n\ndef helper():\n Counter() # error: `helper` is not a composable\n\n@composable\ndef App():\n Button(\"x\"):\n Text(\"clicked\") # error: a handler runs after composition\n\ndef main():\n run_app(\"app\"):\n App() # ok: the root of the composition\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"conditional-slot": {
"title": "detects a basedpython-ui slot created under a condition",
"description": "## What it does\n\nChecks for a `basedpython_ui` slot — `state`, `state_list`, `state_dict`, `derived`, `remember`,\n`launched_effect`, `disposable_effect`, `side_effect` — created under a condition in a composable:\ninside an `if`, `for`, `while`, `try` or `match`, inside a comprehension, or inside a block that is\nnot a `once` content block (a handler block, a lambda, a nested `def`).\n\n## Why is this bad?\n\nA slot lives as long as its enclosing composition scope and is identified by its call site, so a\nconditional slot is created when the condition first holds and disposed — its state lost, its\neffect cancelled — as soon as it stops holding. That is rarely what the code means: state that\nshould outlive a condition belongs above it, and a slot created from a handler has no scope to live\nin at all. The runtime keys slots by call site, so this is safe at runtime; the check makes the\nlifetime visible.\n\n## Examples\n\n```by\nfrom basedpython_ui import composable, state, Text\n\n@composable\ndef Profile(show: bool):\n if show:\n let clicks = state(0) # warning: created and disposed as `show` changes\n Text(f\"{clicks.value}\")\n\n@composable\ndef Fixed(show: bool):\n let clicks = state(0) # ok: lives as long as `Fixed`\n if show:\n Text(f\"{clicks.value}\")\n```",
"default": "warn",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"conflicting-declarations": {
"title": "detects conflicting declarations",
"description": "## What it does\n\nChecks whether a variable has been declared as two conflicting types.\n\n## Why is this bad\n\nA variable with two conflicting declarations likely indicates a mistake. Moreover, it could lead to\nincorrect or ill-defined type inference for other code that relies on these variables.\n\n## Examples\n\n```python\nif __name__ == \"__main__\":\n a: int\nelse:\n a: str\n\na = 1 # error\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"conflicting-metaclass": {
"title": "detects conflicting metaclasses",
"description": "## What it does\n\nChecks for class definitions where the metaclass of the class being created would not be a subclass\nof the metaclasses of all the class's bases.\n\n## Why is it bad?\n\nSuch a class definition raises a `TypeError` at runtime.\n\n## Examples\n\n```pyi\nclass M1(type): ...\nclass M2(type): ...\nclass A(metaclass=M1): ...\nclass B(metaclass=M2): ...\n\n# TypeError: metaclass conflict\nclass C(A, B): ... # error\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"content-block-control-flow": {
"title": "detects a `return` inside a nested `once` content block",
"description": "## What it does\n\nChecks for a `return` inside a `once` content block that is itself written inside another\ntrailing-lambda block.\n\n## Why is this bad?\n\nA `once` block runs exactly once, inline, so a `return` inside it is allowed to leave the enclosing\nscope — but only one level: the language propagates a block's `return` to the scope the block is\nwritten in. When that scope is itself a block, the `return` leaves the inner block and stops there;\nthe enclosing function keeps running, and the returned value is silently discarded.\n\n(A `break` or `continue` inside any block is already rejected as `break` outside loop: a block is\nits own function.)\n\n## Examples\n\n```by\ndef Column(once content: () -> None):\n content()\n\ndef Row(once content: () -> None):\n content()\n\ndef App(done: bool) -> int:\n Column:\n Row:\n if done:\n return 1 # error: [content-block-control-flow]\n return 2 # ok: one level, leaves `App`\n return 0\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"cyclic-class-definition": {
"title": "detects cyclic class definitions",
"description": "## What it does\n\nChecks for class definitions in stub files that inherit (directly or indirectly) from themselves.\n\n## Why is it bad?\n\nAlthough forward references are natively supported in stub files, inheritance cycles are still\ndisallowed, as it is impossible to resolve a consistent [method resolution order] for a class that\ninherits from itself.\n\n## Examples\n\n`foo.pyi`:\n\n```pyi\nclass A(B): ... # error\nclass B(A): ... # error\n```\n\n[method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"cyclic-type-alias-definition": {
"title": "detects cyclic type alias definitions",
"description": "## What it does\n\nChecks for circular type alias definitions.\n\n## Why is it bad?\n\nRecursive aliases are valid when recursive references occur inside another type, such as\n`list[Tree]`. An alias cannot expand directly to itself or include itself as a union member. This\napplies to both `type` statements and aliases created with `TypeAliasType`.\n\n## Examples\n\n```toml\n[environment]\npython-version = \"3.12\"\n```\n\n```python\nfrom typing import TypeAliasType\n\ntype Itself = Itself # error\n\ntype A = B # error\ntype B = A # error\n\ntype IntOr = int | IntOr # error\n\nCycle = TypeAliasType(\"Cycle\", \"Cycle\") # error\n\ntype Tree = int | list[Tree] # valid recursive alias\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"dataclass-field-order": {
"title": "detects dataclass definitions with required fields after fields with default values",
"description": "## What it does\n\nChecks for dataclass definitions where required fields are defined after fields with default values.\n\n## Why is this bad?\n\nIn dataclasses, all required fields (fields without default values) must be defined before fields\nwith default values. This is a Python requirement that will raise a `TypeError` at runtime if\nviolated.\n\n## Example\n\n```python\nfrom dataclasses import dataclass\n\n\n@dataclass\nclass Example:\n x: int = 1 # Field with default value\n # Required field after field with default\n y: str # error\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"deprecated": {
"title": "detects uses of deprecated items",
"description": "## What it does\n\nChecks for uses of deprecated items\n\n## Why is this bad?\n\nDeprecated items should no longer be used.\n\n## Examples\n\n```toml\n[environment]\npython-version = \"3.13\"\n```\n\n```python\nimport warnings\n\n\[email protected](\"use new_func instead\")\ndef old_func(): ...\n\n\nold_func() # error: [deprecated]\n```",
"default": "warn",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"disjoint-cast": {
"title": "detects `cast` calls between disjoint types",
"description": "## What it does\n\nDetects `cast` calls where the inferred type of the value is disjoint from the destination type.\n\nTwo types are disjoint if they are entirely non-overlapping. For example, `str` and `int` are\ndisjoint types because it is impossible to create a Python object that is both a `str` and an `int`\nat the same time: Python forbids multiple inheritance between these two classes:\n\n```pycon\n>>> class StrAndInt(int, str): ...\nTraceback (most recent call last):\n File \"<python-input-0>\", line 1, in <module>\n class StrAndInt(int, str): ...\nTypeError: multiple bases have instance lay-out conflict\n```\n\nThis means that any object of type `int` can never also be of type `str`, and any object of type\n`str` can never also inhabit the type `int`. The only common subtype of these two types is\n[`Never`][never], the uninhabited type, which has no members.\n\n## Why is this bad?\n\n`cast()` is deliberately designed as an \"escape hatch\" in the type system that is neither validated\nat runtime nor, by default, by type checkers. While upcasting to a supertype is always sound, and\ncasting to a subtype can be sound in some situations if accompanied by careful validation checks,\n`cast()` is also deliberately designed to allow unsound narrowing, and most useful applications of\n`cast()` in real-world code cannot be fully validated by a type checker.\n\nNonetheless, even while acknowledging the fact that `cast()` is intentionally designed to allow\nunsoundness, casting a value to an entirely *disjoint* type is especially likely to indicate a\nmistake in your code. A cast from an `int` to a `str`, for example, likely indicates a bug or\nmisunderstanding.\n\nThis rule therefore provides a means for codebases to partially validate their uses of `cast()`\nwithout banning the API -- or even banning all unsound uses of the API -- entirely.\n\n## Example\n\n```py\nfrom typing import cast\n\n\ndef parse(value: int) -> str:\n return cast(str, value) # error: [disjoint-cast]\n```\n\nCasts between overlapping (non-disjoint) types are allowed:\n\n```py\nfrom collections.abc import Sequence\nfrom typing import cast\n\n\ndef validate(numbers: Sequence[int | None]) -> Sequence[int]:\n if None in numbers:\n raise TypeError(\"must provide a sequence of numbers!\")\n return cast(Sequence[int], numbers)\n```\n\nNote that disjointness between types can sometimes be surprising. For example, `list[int]` is\ndisjoint from `list[bool]` even though `bool` is a subtype of `int`. Due to the fact that `list` is\n[mutable and invariant], it would be deeply unsound for ty to ever narrow an object of type\n`list[int]` to the type `list[bool]`. As such, ty will complain about a cast from `list[int]` to\n`list[bool]` when this rule is enabled.\n\nSimilarly, two `NewType`s can be disjoint even when they share the same underlying nominal base\ntype, unless one `NewType` is explicitly declared as a sub-newtype of the other.\n\n```py\nfrom typing import NewType, cast\n\n\nUserId = NewType(\"UserId\", int)\nProUserId = NewType(\"ProUserId\", int)\n\n\ndef f(x: list[int], user_id: UserId):\n y = cast(list[bool], x) # error: [disjoint-cast]\n pro_user_id = cast(ProUserId, user_id) # error: [disjoint-cast]\n```\n\n## Alternatives\n\nIn many cases, the diagnostic can be avoided by switching to use covariant generic types rather than\ninvariant ones:\n\n```py\n# `Sequence`, unlike `list`, is immutable and covariant\nfrom collections.abc import Sequence\nfrom typing import cast\n\n\ndef f(x: Sequence[int]):\n y = cast(Sequence[bool], x) # no diagnostic\n```\n\nThough if you're able to use covariant types, a type-safe narrowing mechanism that provides runtime\nvalidation, such as using `TypeIs`, is generally preferable to using `cast`:\n\n```py\n# `Sequence`, unlike `list`, is immutable and covariant\nfrom collections.abc import Sequence\nfrom typing_extensions import TypeIs, reveal_type\n\n\ndef is_sequence_of_bools(x: Sequence[int]) -> TypeIs[Sequence[bool]]:\n return all(isinstance(item, bool) for item in x)\n\n\ndef f(x: Sequence[int]):\n assert is_sequence_of_bools(x)\n reveal_type(x) # revealed: Sequence[bool]\n```\n\nIf you're unable to switch to an immutable, covariant generic type, other solutions to this\nparticular diagnostic might include assigning a new list altogether:\n\n```py\ndef f(x: list[int]):\n y: list[bool] = []\n for item in x:\n assert isinstance(item, bool)\n y.append(item)\n```\n\nOr using a `TypeGuard`. While the \"narrowing\" below is still unsound, there is at least some runtime\nvalidation of the element types taking place, making it superior to the `cast`:\n\n```py\nfrom typing_extensions import TypeGuard, reveal_type\n\n\ndef is_list_of_bools(x: list[int]) -> TypeGuard[list[bool]]:\n return all(isinstance(item, bool) for item in x)\n\n\ndef f(x: list[int]):\n assert is_list_of_bools(x)\n reveal_type(x) # revealed: list[bool]\n```\n\n## Default level\n\nThis rule is disabled by default. It is designed as a strict rule for users who want additional\nsoundness checks from their type checker, and it may have false positives in some situations.\n\n## See also\n\n- The Ruff rule [`banned-api`][banned-api] can be used to ban the use of `cast()` entirely in your\n codebase.\n- `redundant-cast` detects casts where the value already has the destination type.\n\n[banned-api]: https://docs.astral.sh/ruff/rules/banned-api/\n[mutable and invariant]: https://docs.astral.sh/ty/reference/typing-faq/#invariant-generics\n[never]: https://docs.python.org/3/library/typing.html#typing.Never",
"default": "ignore",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"division-by-zero": {
"title": "detects division by zero",
"description": "## What it does\n\nIt detects division by zero.\n\n## Why is this bad?\n\nDividing by zero raises a `ZeroDivisionError` at runtime.\n\n## Rule status\n\nThis rule is currently disabled by default because of the number of false positives it can produce.\n\n## Examples\n\n```python\n5 / 0 # error\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"duplicate-base": {
"title": "detects class definitions with duplicate bases",
"description": "## What it does\n\nChecks for class definitions with duplicate bases.\n\n## Why is this bad?\n\nClass definitions with duplicate bases raise `TypeError` at runtime.\n\n## Examples\n\n```python\nclass A: ...\n\n\n# TypeError: duplicate base class\nclass B(A, A): ... # error\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"duplicate-kw-only": {
"title": "detects dataclass definitions with more than one usage of `KW_ONLY`",
"description": "## What it does\n\nChecks for dataclass definitions with more than one field annotated with `KW_ONLY`.\n\n## Why is this bad?\n\n`dataclasses.KW_ONLY` is a special marker used to emulate the `*` syntax in normal signatures. It\ncan only be used once per dataclass.\n\nAttempting to annotate two different fields with it will lead to a runtime error.\n\n## Examples\n\n```python\nfrom dataclasses import dataclass, KW_ONLY\n\n\n# Crash at runtime\n@dataclass\nclass A: # error\n b: int\n _1: KW_ONLY\n c: str\n _2: KW_ONLY\n d: bytes\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"dynamic-function-decorator-return": {
"title": "detects decorators that replace a function with a dynamic type such as `Any`",
"description": "## What it does\n\nDetects decorator applications that replace a function with `Any` or another [dynamic type].\n\n## Why is this bad?\n\nA decorator can replace the function it receives with any object. Type checkers therefore use the\ndecorator's return type as the type of the decorated function. If the decorator returns `Any` or\n`Unknown`, the original type is lost, along with the type checker's ability to catch invalid calls\nand attribute accesses. basedpython infers an unannotated return, so a decorator reaches this state\nby saying `Any` outright, or by coming from code the checker cannot read:\n\n```py\nfrom collections.abc import Callable\nfrom typing import Any\n\n\ndef untyped_decorator(function: Callable[..., object]) -> Any:\n return function\n\n\n# error: \"Decorator returns `Any`\"\n@untyped_decorator\ndef stringify(value: int) -> str:\n return str(value)\n\n\n# No type error is reported, even though `stringify` expects an integer.\nstringify(\"not an integer\")\n```\n\nThis rule identifies the point where a decorator erases useful type information, before that\nimprecision spreads to every use of the decorated function. It can be especially useful in cases\nwhere the decorator is defined in a third-party library. Whereas linter rules such as\n[`ANN201`][ann201] and [`ANN202`][ann202] can complain about missing annotations in your first-party\ncode, they cannot identify instances where unsound types leak into your code due to missing type\nannotations in third-party code installed into `site-packages`.\n\n## Examples\n\n`third_party_library.py`:\n\n```py\nfrom collections.abc import Callable\nfrom typing import Any\n\n\ndef untyped_decorator(function: Callable[..., object]) -> Any:\n return function\n```\n\n`first_party.py`:\n\n```py\nfrom third_party_library import untyped_decorator\n\n\n# error: \"Decorator returns `Any`\"\n@untyped_decorator\ndef greet(name: str) -> str:\n return f\"Hello, {name}!\"\n```\n\nIf making a PR to the third-party library to improve their annotations is not possible, fixes for\nthis diagnostic could include writing your own decorator or introducing a type-safe wrapper:\n\n```py\nfrom collections.abc import Callable\nfrom typing import TypeVar\n\nfrom third_party_library import untyped_decorator\n\n\nFunctionT = TypeVar(\"FunctionT\", bound=Callable[..., object])\n\n\ndef typed_wrapper(f: FunctionT) -> FunctionT:\n decorated = untyped_decorator(f)\n assert decorated is f\n return decorated\n\n\n@typed_wrapper\ndef greet(name: str) -> str:\n return f\"Hello, {name}!\"\n```\n\n## Default level\n\nThis rule is disabled by default. It is intended for advanced users wanting additional soundness\nchecks from their type checker, not for users who have just started to use type checkers on their\nPython code.\n\n[ann201]: https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/\n[ann202]: https://docs.astral.sh/ruff/rules/missing-return-type-private-function/\n[dynamic type]: https://typing.python.org/en/latest/spec/glossary.html#term-dynamic-type",
"default": "ignore",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"empty-body": {
"title": "detects functions with empty bodies that have a non-`None` return type annotation",
"description": "## What it does\n\nDetects functions with empty bodies that have a non-`None` return type annotation.\n\nThe errors reported by this rule have the same motivation as the `invalid-return-type` rule. The\ndiagnostic exists as a separate error code to allow users to disable this rule while prototyping\ncode. While we strongly recommend enabling this rule if possible, users migrating from other type\ncheckers may also find it useful to temporarily disable this rule on some or all of their codebase\nif they find it results in a large number of diagnostics.\n\n## Why is this bad?\n\nA function with an empty body (containing only `...`, `pass`, or a docstring) will implicitly return\n`None` at runtime. Returning `None` when the return type is non-`None` is unsound, and will lead to\nty inferring incorrect types elsewhere.\n\nFunctions with empty bodies are permitted in certain contexts where they serve as declarations\nrather than implementations:\n\n- Functions in stub files (`.pyi`)\n- Methods in Protocol classes\n- Abstract methods decorated with `@abstractmethod`\n- Overload declarations decorated with `@overload`\n- Functions in `if TYPE_CHECKING` blocks\n\n## Examples\n\n```python\ndef foo() -> int: ... # error: [empty-body]\n\n\ndef bar() -> str: # error: [empty-body]\n \"\"\"A function that does nothing.\"\"\"\n pass\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"erased-cast-argument": {
"title": "detects casts whose type arguments cannot be checked at runtime",
"description": "## What it does\nChecks for a basedpython `cast` / `cast?` whose target type carries type\narguments that are erased at runtime.\n\n## Why is this bad?\nA checked cast validates its value with `isinstance`, which can only test\na class — a builtin container erases its type arguments, so `list[int]`\nis checkable only as `list`. The cast still narrows the static type to\n`list[int]`, but nothing verifies the `int` claim at runtime, which is\nexactly the assumption a checked cast exists to rule out.\n\nThis only fires where the claim really is assumed. A *user* generic\ncarries `__orig_class__`, so `A[int]` is checked in full. A value typed by\na *reified* type parameter carries the answer in a runtime cell, so\ncasting `list[T]` to `list[int]` compares `T == int` exactly. A *protocol*\nis checked structurally against the value's reified annotations — data\nmembers against class annotations, method members against\nparameter/return annotations. Only a protocol member whose specialized\ntype has no runtime spelling (a callable attribute) leaves the cast with\nno runtime residue, so the whole cast — not just its arguments — is left\nunchecked.\n\n## Example\n\n```by\nfrom typing import Protocol\nfrom collections.abc import Callable\n\ndef f(x: object):\n a = x cast! list[int] # warning: only `list` is checked\n b = x cast! list # ok — no argument claimed\n\nclass A[T]:\n init(self, t: T)\n\ndef g(x: object):\n a = x cast! A[int] # ok — checked in full via `__orig_class__`\n\ndef r[T](data: list[T]):\n a = data cast! list[int] # ok — the reified `T` cell decides it\n\nclass HasCb[T](Protocol):\n cb: Callable[[T], T]\n\ndef h(x: object):\n a = x cast! HasCb[int] # warning: a callable member has no runtime check\n```",
"default": "warn",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"erased-type-check": {
"title": "detects parametric type tests with no runtime residue",
"description": "## What it does\nChecks for parametric type tests (`x is P[int]`) that have no sound\nruntime residue — against a protocol, or against a class the runtime\nrefuses to subscript.\n\n## Why is this bad?\nA parametric `is` test is answered from static types wherever possible\n(Rust-style). When it cannot be — the value's type is dynamic or a mixed\nunion — the last resort is a runtime probe that unwinds the value's\n`__orig_class__` and its class's generic bases across the mro. A protocol\nhas nothing to unwind: an instance's `__orig_class__` names its concrete\nclass, never the protocol, and a structural `isinstance` check sees no\ntype arguments (and raises outright unless the protocol is\n`@runtime_checkable`). So the test can never confirm the specialization.\n\nThe probe also has to name the target specialization at runtime, and a\nclass can be generic to a type checker well before the runtime lets you\nsubscript it: `array.array` only grew a `__class_getitem__` in 3.12, and\n`memoryview` in 3.14. Below those versions, evaluating the probe's target\nraises `TypeError` instead of answering the test.\n\n## Example\n\n```by\nfrom typing import Protocol\nclass P[T](Protocol):\n def get(self) -> T: ...\n\ndef f(x):\n return x is P[int] # error: a protocol records no specialization\n```\n\n```by\n# on a target below python 3.12\nimport array\n\ndef g(x):\n return x is array.array[int] # error: not subscriptable at runtime\n```\n\nReify the type parameter (so the test compares the reified cell), or test\nagainst a concrete class that fixes the arguments (a user generic, or a\nsubclass whose `__orig_bases__` records the specialization):\n\n```by\ndef f[T](x: T):\n return x is list[int] # ok — compares the reified `T`\n\nclass A[T]: ...\ndef g(x):\n return x is A[int] # ok — unwinds `x`'s mro\n```\n\nA target that isn't subscriptable at runtime still has a bare-class test:\n\n```by\nimport array\n\ndef h(x):\n return x is array.array # ok — an ordinary `isinstance`\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"escape-character-in-forward-annotation": {
"title": "detects forward type annotations with escape characters",
"description": "## What it does\n\nChecks for forward annotations that contain escape characters.\n\n## Why is this bad?\n\nStatic analysis tools like ty can't analyze type annotations that contain escape characters.\n\n## Example\n\n```python\ndef foo() -> \"intt\\b\": ... # error\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"escaping-local": {
"title": "detects a `local` parameter whose value escapes its call",
"description": "## What it does\nChecks for a basedpython `local` parameter whose value escapes the call it\nis bound in — returned to the caller, stored on a parameter-rooted object,\nassigned to a `global` / `nonlocal` binding, or passed on to a parameter\nthat is not itself a borrow.\n\nA callable type may declare its own parameters `local` too\n(`(local int) -> None`), which puts the same constraint on the trailing\nlambda block filling it: the block's implicit `it` is borrowed from the\ncall.\n\n## Why is this bad?\nA `local` parameter is borrowed only for the duration of the call. Letting\nits value outlive the call defeats the borrow: the caller may release the\nunderlying resource, leaving a dangling reference behind.\n\n## Example\n\n```by\n_saved: object\n\ndef f(local fn: () -> None):\n global _saved\n _saved = fn # error: `fn` is local and cannot escape the call\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"escaping-loop-variable": {
"title": "detects a trailing-lambda block capturing a loop variable with a non-borrow callee",
"description": "## What it does\nChecks for a trailing-lambda block inside a loop that captures a loop\nvariable while its callee's callback parameter is **not** a borrow\n(`local` / `once`).\n\n## Why is this bad?\nA trailing-lambda block lowers to a closure that captures the loop variable\nby reference. If the callee is a borrow (`local` / `once`), it runs the\nblock synchronously — the variable still holds this iteration's value. But\na non-borrow callee may store the block and call it after the loop has\nadvanced, at which point every deferred call sees the loop variable's final\nvalue — the classic late-binding trap.\n\nThis is the type-aware complement to ruff's syntactic `B023`, which cannot\nresolve the callee's marker. An opaque callee (not a resolvable function or\nbound method) is left alone.\n\n## Example\n\n```by\ndef defer(fn: () -> None): # not a borrow — may keep `fn`\n _saved.append(fn)\n\nfor x in [1, 2, 3]:\n defer:\n print(x) # error: captures loop variable `x`\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"experimental-syntax": {
"title": "detects experimental syntax",
"description": "## What it does\n\nChecks for experimental syntax that is not part of the Python typing specification.\n\n## Why is this bad?\n\nExperimental syntax is specific to ty. It may be rejected by other type checkers and may never be\nstandardized, or be subject to breaking changes.\n\n## Examples\n\n```toml\n[environment]\npython-version = \"3.14\"\n```\n\n```python\nclass A: ...\n\n\nclass B: ...\n\n\ndef f(value: A & B) -> None: ... # error: [experimental-syntax]\ndef g(value: ~A) -> None: ... # error: [experimental-syntax]\n```",
"default": "warn",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"final-on-non-method": {
"title": "detects `@final` applied to non-method functions",
"description": "## What it does\n\nChecks for `@final` decorators applied to non-method functions.\n\n## Why is this bad?\n\nThe `@final` decorator is only meaningful on methods and classes. Applying it to a module-level\nfunction or a nested function has no effect and is likely a mistake.\n\n## Example\n\n```python\nfrom typing import final\n\n\n# @final is not allowed on non-method functions\n@final # error\ndef my_function() -> int:\n return 0\n```",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}