Skip to content

Commit cfb849a

Browse files
bpo-47088: Add typing.LiteralString (PEP 675) (GH-32064)
Co-authored-by: Nick Pope <[email protected]>
1 parent a755124 commit cfb849a

File tree

4 files changed

+116
-2
lines changed

4 files changed

+116
-2
lines changed

Doc/library/typing.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ annotations. These include:
7676
*Introducing* :data:`TypeGuard`
7777
* :pep:`673`: Self type
7878
*Introducing* :data:`Self`
79+
* :pep:`675`: Arbitrary Literal String Type
80+
*Introducing* :data:`LiteralString`
7981

8082
.. _type-aliases:
8183

@@ -585,6 +587,33 @@ These can be used as types in annotations and do not support ``[]``.
585587
avoiding type checker errors with classes that can duck type anywhere or
586588
are highly dynamic.
587589

590+
.. data:: LiteralString
591+
592+
Special type that includes only literal strings. A string
593+
literal is compatible with ``LiteralString``, as is another
594+
``LiteralString``, but an object typed as just ``str`` is not.
595+
A string created by composing ``LiteralString``-typed objects
596+
is also acceptable as a ``LiteralString``.
597+
598+
Example::
599+
600+
def run_query(sql: LiteralString) -> ...
601+
...
602+
603+
def caller(arbitrary_string: str, literal_string: LiteralString) -> None:
604+
run_query("SELECT * FROM students") # ok
605+
run_query(literal_string) # ok
606+
run_query("SELECT * FROM " + literal_string) # ok
607+
run_query(arbitrary_string) # type checker error
608+
run_query( # type checker error
609+
f"SELECT * FROM students WHERE name = {arbitrary_string}"
610+
)
611+
612+
This is useful for sensitive APIs where arbitrary user-generated
613+
strings could generate problems. For example, the two cases above
614+
that generate type checker errors could be vulnerable to an SQL
615+
injection attack.
616+
588617
.. data:: Never
589618

590619
The `bottom type <https://en.wikipedia.org/wiki/Bottom_type>`_,

Lib/test/test_typing.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from typing import IO, TextIO, BinaryIO
2828
from typing import Pattern, Match
2929
from typing import Annotated, ForwardRef
30-
from typing import Self
30+
from typing import Self, LiteralString
3131
from typing import TypeAlias
3232
from typing import ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
3333
from typing import TypeGuard
@@ -265,6 +265,60 @@ def test_alias(self):
265265
self.assertEqual(get_args(alias_3), (Self,))
266266

267267

268+
class LiteralStringTests(BaseTestCase):
269+
def test_equality(self):
270+
self.assertEqual(LiteralString, LiteralString)
271+
self.assertIs(LiteralString, LiteralString)
272+
self.assertNotEqual(LiteralString, None)
273+
274+
def test_basics(self):
275+
class Foo:
276+
def bar(self) -> LiteralString: ...
277+
class FooStr:
278+
def bar(self) -> 'LiteralString': ...
279+
class FooStrTyping:
280+
def bar(self) -> 'typing.LiteralString': ...
281+
282+
for target in [Foo, FooStr, FooStrTyping]:
283+
with self.subTest(target=target):
284+
self.assertEqual(gth(target.bar), {'return': LiteralString})
285+
self.assertIs(get_origin(LiteralString), None)
286+
287+
def test_repr(self):
288+
self.assertEqual(repr(LiteralString), 'typing.LiteralString')
289+
290+
def test_cannot_subscript(self):
291+
with self.assertRaises(TypeError):
292+
LiteralString[int]
293+
294+
def test_cannot_subclass(self):
295+
with self.assertRaises(TypeError):
296+
class C(type(LiteralString)):
297+
pass
298+
with self.assertRaises(TypeError):
299+
class C(LiteralString):
300+
pass
301+
302+
def test_cannot_init(self):
303+
with self.assertRaises(TypeError):
304+
LiteralString()
305+
with self.assertRaises(TypeError):
306+
type(LiteralString)()
307+
308+
def test_no_isinstance(self):
309+
with self.assertRaises(TypeError):
310+
isinstance(1, LiteralString)
311+
with self.assertRaises(TypeError):
312+
issubclass(int, LiteralString)
313+
314+
def test_alias(self):
315+
alias_1 = Tuple[LiteralString, LiteralString]
316+
alias_2 = List[LiteralString]
317+
alias_3 = ClassVar[LiteralString]
318+
self.assertEqual(get_args(alias_1), (LiteralString, LiteralString))
319+
self.assertEqual(get_args(alias_2), (LiteralString,))
320+
self.assertEqual(get_args(alias_3), (LiteralString,))
321+
268322
class TypeVarTests(BaseTestCase):
269323
def test_basic_plain(self):
270324
T = TypeVar('T')

Lib/typing.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def _idfunc(_, x):
126126
'get_origin',
127127
'get_type_hints',
128128
'is_typeddict',
129+
'LiteralString',
129130
'Never',
130131
'NewType',
131132
'no_type_check',
@@ -180,7 +181,7 @@ def _type_check(arg, msg, is_argument=True, module=None, *, allow_special_forms=
180181
if (isinstance(arg, _GenericAlias) and
181182
arg.__origin__ in invalid_generic_forms):
182183
raise TypeError(f"{arg} is not valid as type argument")
183-
if arg in (Any, NoReturn, Never, Self, TypeAlias):
184+
if arg in (Any, LiteralString, NoReturn, Never, Self, TypeAlias):
184185
return arg
185186
if allow_special_forms and arg in (ClassVar, Final):
186187
return arg
@@ -523,6 +524,34 @@ def returns_self(self) -> Self:
523524
raise TypeError(f"{self} is not subscriptable")
524525

525526

527+
@_SpecialForm
528+
def LiteralString(self, parameters):
529+
"""Represents an arbitrary literal string.
530+
531+
Example::
532+
533+
from typing import LiteralString
534+
535+
def run_query(sql: LiteralString) -> ...
536+
...
537+
538+
def caller(arbitrary_string: str, literal_string: LiteralString) -> None:
539+
run_query("SELECT * FROM students") # ok
540+
run_query(literal_string) # ok
541+
run_query("SELECT * FROM " + literal_string) # ok
542+
run_query(arbitrary_string) # type checker error
543+
run_query( # type checker error
544+
f"SELECT * FROM students WHERE name = {arbitrary_string}"
545+
)
546+
547+
Only string literals and other LiteralStrings are compatible
548+
with LiteralString. This provides a tool to help prevent
549+
security issues such as SQL injection.
550+
551+
"""
552+
raise TypeError(f"{self} is not subscriptable")
553+
554+
526555
@_SpecialForm
527556
def ClassVar(self, parameters):
528557
"""Special type construct to mark class variables.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implement :data:`typing.LiteralString`, part of :pep:`675`. Patch by Jelle
2+
Zijlstra.

0 commit comments

Comments
 (0)