Skip to content

[WIP] introduce a distinct bytes type. #580

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 105 additions & 15 deletions stdlib/2.7/__builtin__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ class int(SupportsInt, SupportsFloat, SupportsAbs[int]):
def __abs__(self) -> int: ...
def __hash__(self) -> int: ...

long = int
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just moved this to a more logical place.


class float(SupportsFloat, SupportsInt, SupportsAbs[float]):
@overload
def __init__(self) -> None: ...
Expand Down Expand Up @@ -210,7 +212,7 @@ class unicode(basestring, Sequence[unicode]):
def center(self, width: int, fillchar: unicode = u' ') -> unicode: ...
def count(self, x: unicode) -> int: ...
def decode(self, encoding: unicode = ..., errors: unicode = ...) -> unicode: ...
def encode(self, encoding: unicode = ..., errors: unicode = ...) -> str: ...
def encode(self, encoding: unicode = ..., errors: unicode = ...) -> bytes: ...
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encode() is just about the strongest signal that we want bytes, apart from using b'' literals.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should things like encoding have the type Text or NativeStr (or even str)? Consider various cases:

  1. encoding: Text. If we have 'x' -> NativeStr then u'foo'.encode('utf8') would be a type error, since NativeStr is not compatible with Text.
  2. encoding: NativeStr. Then u'foo'.encode(u'utf8') would result in a false positive.
  3. encoding: str. Then we really don't have any type checks, any string-like object will be OK.

The idea of adding NativeStr was that in PY2 it represents cases where an implicit unicode->bytes ASCII conversion takes place. And this is the case for encoding in unicode.encode().

But having to write u'foo'.encode(NativeStr(u'utf8')) is not that convenient. (Here the ASCII types proposal would have worked better, but only when you use literals).

On the other hand, if you have your encoding name as a string parameter (as opposed to a literal, and maybe it's a more important case overall) and you make encoding in unicode.encode() to be of type NativeStr (as its run-time semantics suggests), then the cases are like that:

  1. def f(encoding: Text): u'foo'.encode(encoding). You have to use u'foo'.encode(encoding.encode('ascii') if PY2 else encoding). Here you have to make your implicit conversion explicit by saying that you believe that this text can always be ASCII-converted to a 8-bit string. This way you make your code PY2+3 compatible.
  2. def f(encoding: NativeStr): u'foo'.encode(encoding). Everything looks good here.
  3. def f(encoding: str): u'foo'.encode(encoding). Again everything is OK, but it looks like another way of saying that you are sure that you are ASCII-compatible here.

So, a possible conclusion from this is: if you are sure that your argument gets implicitly ASCII-converted to a 8-bit string, then you should use NativeStr for it. If you want to state that you don't really care about checking for implicit ASCII conversions (and PY2+3 compatibility in general), you should use str.

def endswith(self, suffix: Union[unicode, Tuple[unicode, ...]], start: int = 0,
end: int = ...) -> bool: ...
def expandtabs(self, tabsize: int = 8) -> unicode: ...
Expand Down Expand Up @@ -282,7 +284,7 @@ class str(basestring, Sequence[str]):
def center(self, width: int, fillchar: str = ...) -> str: ...
def count(self, x: unicode) -> int: ...
def decode(self, encoding: unicode = ..., errors: unicode = ...) -> unicode: ...
def encode(self, encoding: unicode = ..., errors: unicode = ...) -> str: ...
def encode(self, encoding: unicode = ..., errors: unicode = ...) -> bytes: ...
def endswith(self, suffix: Union[unicode, Tuple[unicode, ...]]) -> bool: ...
def expandtabs(self, tabsize: int = 8) -> str: ...
def find(self, sub: unicode, start: int = 0, end: int = 0) -> int: ...
Expand Down Expand Up @@ -366,11 +368,102 @@ class str(basestring, Sequence[str]):
def __ge__(self, x: unicode) -> bool: ...
def __mod__(self, x: Any) -> str: ...

class bytes(basestring, Sequence[bytes]):
# TODO: double-check unicode, AnyStr, unions, bytearray
Copy link
Member Author

@gvanrossum gvanrossum Oct 1, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far this is just a clone of str() above with all occurrences of str replaced by bytes.

def __init__(self, object: object) -> None: ...
def capitalize(self) -> bytes: ...
def center(self, width: int, fillchar: bytes = ...) -> bytes: ...
def count(self, x: unicode) -> int: ...
def decode(self, encoding: unicode = ..., errors: unicode = ...) -> unicode: ...
def encode(self, encoding: unicode = ..., errors: unicode = ...) -> bytes: ...
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, bytes probably shouldn't have encode() at all.

def endswith(self, suffix: Union[unicode, Tuple[unicode, ...]]) -> bool: ...
Copy link
Member Author

@gvanrossum gvanrossum Oct 1, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably use Union[bytes, Tuple[bytes, ...]], and similar for most other uses of unicode in this class.

def expandtabs(self, tabsize: int = 8) -> bytes: ...
def find(self, sub: unicode, start: int = 0, end: int = 0) -> int: ...
def format(self, *args: Any, **kwargs: Any) -> bytes: ...
def index(self, sub: unicode, start: int = 0, end: int = 0) -> int: ...
def isalnum(self) -> bool: ...
def isalpha(self) -> bool: ...
def isdigit(self) -> bool: ...
def islower(self) -> bool: ...
def isspace(self) -> bool: ...
def istitle(self) -> bool: ...
def isupper(self) -> bool: ...
def join(self, iterable: Iterable[AnyStr]) -> AnyStr: ...
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably should just be bytes as well, no need for AnyStr here. bytes can only be joined with bytes. Right?

def ljust(self, width: int, fillchar: bytes = ...) -> bytes: ...
def lower(self) -> bytes: ...
@overload
def lstrip(self, chars: bytes = ...) -> bytes: ...
@overload
def lstrip(self, chars: unicode) -> unicode: ...
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No.

@overload
def partition(self, sep: bytearray) -> Tuple[bytes, bytearray, bytes]: ...
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was an existing overload for bytearray in str as well. I think it's pretty unfortunate; we probably either will have to add a lot more of these, or get rid of them and do some form of auto-promotion of bytearray to bytes.

@overload
def partition(self, sep: bytes) -> Tuple[bytes, bytes, bytes]: ...
@overload
def partition(self, sep: unicode) -> Tuple[unicode, unicode, unicode]: ...
def replace(self, old: AnyStr, new: AnyStr, count: int = ...) -> AnyStr: ...
def rfind(self, sub: unicode, start: int = 0, end: int = 0) -> int: ...
def rindex(self, sub: unicode, start: int = 0, end: int = 0) -> int: ...
def rjust(self, width: int, fillchar: bytes = ...) -> bytes: ...
@overload
def rpartition(self, sep: bytearray) -> Tuple[bytes, bytearray, bytes]: ...
@overload
def rpartition(self, sep: bytes) -> Tuple[bytes, bytes, bytes]: ...
@overload
def rpartition(self, sep: unicode) -> Tuple[unicode, unicode, unicode]: ...
@overload
def rsplit(self, sep: Optional[bytes] = ..., maxsplit: int = ...) -> List[bytes]: ...
@overload
def rsplit(self, sep: unicode, maxsplit: int = ...) -> List[unicode]: ...
@overload
def rstrip(self, chars: bytes = ...) -> bytes: ...
@overload
def rstrip(self, chars: unicode) -> unicode: ...
@overload
def split(self, sep: Optional[bytes] = ..., maxsplit: int = ...) -> List[bytes]: ...
@overload
def split(self, sep: unicode, maxsplit: int = ...) -> List[unicode]: ...
def splitlines(self, keepends: bool = ...) -> List[bytes]: ...
def startswith(self, prefix: Union[unicode, Tuple[unicode, ...]]) -> bool: ...
@overload
def strip(self, chars: bytes = ...) -> bytes: ...
@overload
def strip(self, chars: unicode) -> unicode: ...
def swapcase(self) -> bytes: ...
def title(self) -> bytes: ...
def translate(self, table: Optional[AnyStr], deletechars: AnyStr = ...) -> AnyStr: ...
def upper(self) -> bytes: ...
def zfill(self, width: int) -> bytes: ...

def __len__(self) -> int: ...
def __iter__(self) -> Iterator[bytes]: ...
def __str__(self) -> str: ...
def __repr__(self) -> str: ...
def __int__(self) -> int: ...
def __float__(self) -> float: ...
def __hash__(self) -> int: ...
@overload
def __getitem__(self, i: int) -> bytes: ...
@overload
def __getitem__(self, s: slice) -> bytes: ...
def __getslice__(self, start: int, stop: int) -> bytes: ...
def __add__(self, s: AnyStr) -> AnyStr: ...
def __mul__(self, n: int) -> bytes: ...
def __rmul__(self, n: int) -> bytes: ...
def __contains__(self, o: object) -> bool: ...
def __eq__(self, x: object) -> bool: ...
def __ne__(self, x: object) -> bool: ...
def __lt__(self, x: unicode) -> bool: ...
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely wrong.

def __le__(self, x: unicode) -> bool: ...
def __gt__(self, x: unicode) -> bool: ...
def __ge__(self, x: unicode) -> bool: ...
def __mod__(self, x: Any) -> bytes: ...

class bytearray(MutableSequence[int]):
@overload
def __init__(self) -> None: ...
@overload
def __init__(self, x: Union[Iterable[int], str]) -> None: ...
def __init__(self, x: Union[Iterable[int], bytes]) -> None: ...
@overload
def __init__(self, x: unicode, encoding: unicode,
errors: unicode = ...) -> None: ...
Expand Down Expand Up @@ -659,9 +752,6 @@ class property:
def __set__(self, obj: Any, value: Any) -> None: ...
def __delete__(self, obj: Any) -> None: ...

long = int
bytes = str

NotImplemented = ... # type: Any

def abs(n: SupportsAbs[_T]) -> _T: ...
Expand Down Expand Up @@ -716,11 +806,11 @@ def next(i: Iterator[_T]) -> _T: ...
def next(i: Iterator[_T], default: _T) -> _T: ...
def oct(i: int) -> str: ... # TODO __index__
@overload
def open(file: str, mode: str = 'r', buffering: int = ...) -> BinaryIO: ...
def open(file: str, mode: str = 'r', buffering: int = ...) -> IO[str]: ...
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a the first big dilemma. Python 2's open() always returns a stream that reads/writes str instances (never unicode) but for various reasons BinaryIO derives from IO[bytes], not from IO[str]. BinaryIO adds some extra methods that aren't defined on IO, so we should really have a subclass of IO[str] that adds those same methods. But what to call it? StringIO would be really confusing.

There's of course also the issue that open(file, 'rb') opens a binary file while open(file, 'r') opens a str file -- this wasn't such a big deal in PY2 but it will become one now; eventually we should probably use dependent types to solve that, and in the meantime users will have to use choice casts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it return IO[bytes] in PY2? As you've mentioned, the data in PY2 is always 8-bit strings, so open() clearly behaves way too different in PY2 and PY3 except for open(..., '...b') which is bytes in PY2+3. There are more compatible functions codecs.open() and io.open() where you can return bytes or Text depending on the mode parameter for both PY2+3.

@overload
def open(file: unicode, mode: str = 'r', buffering: int = ...) -> BinaryIO: ...
def open(file: unicode, mode: str = 'r', buffering: int = ...) -> IO[str]: ...
@overload
def open(file: int, mode: str = 'r', buffering: int = ...) -> BinaryIO: ...
def open(file: int, mode: str = 'r', buffering: int = ...) -> IO[str]: ...
def ord(c: unicode) -> int: ...
# This is only available after from __future__ import print_function.
def print(*values: Any, sep: unicode = u' ', end: unicode = u'\n',
Expand Down Expand Up @@ -913,8 +1003,8 @@ class file(BinaryIO):
def __init__(self, file: unicode, mode: str = 'r', buffering: int = ...) -> None: ...
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, maybe we can commandeer 'file' (a PY2-only class) to be the subclass of IO[str] I wished for above.

@overload
def __init__(file: int, mode: str = 'r', buffering: int = ...) -> None: ...
def __iter__(self) -> Iterator[str]: ...
def read(self, n: int = ...) -> str: ...
def __iter__(self) -> Iterator[bytes]: ...
def read(self, n: int = ...) -> bytes: ...
def __enter__(self) -> BinaryIO: ...
def __exit__(self, t: type = None, exc: BaseException = None, tb: Any = None) -> bool: ...
def flush(self) -> None: ...
Expand All @@ -927,10 +1017,10 @@ class file(BinaryIO):
def seekable(self) -> bool: ...
def seek(self, offset: int, whence: int = ...) -> None: ...
def tell(self) -> int: ...
def readline(self, limit: int = ...) -> str: ...
def readlines(self, hint: int = ...) -> List[str]: ...
def write(self, data: str) -> None: ...
def writelines(self, data: Iterable[str]) -> None: ...
def readline(self, limit: int = ...) -> bytes: ...
def readlines(self, hint: int = ...) -> List[bytes]: ...
def write(self, data: bytes) -> None: ...
def writelines(self, data: Iterable[bytes]) -> None: ...
def truncate(self, pos: int = ...) -> int: ...

# Very old builtins
Expand Down
22 changes: 11 additions & 11 deletions stdlib/2.7/io.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,30 @@ class IOBase:
...

class BytesIO(BinaryIO):
def __init__(self, initial_bytes: str = ...) -> None: ...
def __init__(self, initial_bytes: bytes = ...) -> None: ...
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything in this file honestly looks like an improvement (I may even keep it regardless of what we do here, since bytes would just be an alias for str).

# TODO getbuffer
# TODO see comments in BinaryIO for missing functionality
def close(self) -> None: ...
def closed(self) -> bool: ...
def fileno(self) -> int: ...
def flush(self) -> None: ...
def isatty(self) -> bool: ...
def read(self, n: int = ...) -> str: ...
def read(self, n: int = ...) -> bytes: ...
def readable(self) -> bool: ...
def readline(self, limit: int = ...) -> str: ...
def readlines(self, hint: int = ...) -> List[str]: ...
def readline(self, limit: int = ...) -> bytes: ...
def readlines(self, hint: int = ...) -> List[bytes]: ...
def seek(self, offset: int, whence: int = ...) -> None: ...
def seekable(self) -> bool: ...
def tell(self) -> int: ...
def truncate(self, size: int = ...) -> int: ...
def writable(self) -> bool: ...
def write(self, s: str) -> None: ...
def writelines(self, lines: Iterable[str]) -> None: ...
def getvalue(self) -> str: ...
def read1(self) -> str: ...
def write(self, s: bytes) -> None: ...
def writelines(self, lines: Iterable[bytes]) -> None: ...
def getvalue(self) -> bytes: ...
def read1(self) -> bytes: ...

def __iter__(self) -> Iterator[str]: ...
def next(self) -> str: ...
def __iter__(self) -> Iterator[bytes]: ...
def next(self) -> bytes: ...
def __enter__(self) -> 'BytesIO': ...
def __exit__(self, type, value, traceback) -> bool: ...

Expand Down Expand Up @@ -74,7 +74,7 @@ class StringIO(TextIO):

class TextIOWrapper(TextIO):
# write_through is undocumented but used by subprocess
def __init__(self, buffer: IO[str], encoding: unicode = ...,
def __init__(self, buffer: IO[bytes], encoding: unicode = ...,
Copy link
Member Author

@gvanrossum gvanrossum Oct 1, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I've got a feeling that the buffer argument here is actually more restricted than IO[] -- it probably needs to be derived from BufferedIOBase.

errors: unicode = ..., newline: unicode = ...,
line_buffering: bool = ...,
write_through: bool = ...) -> None: ...
Expand Down
36 changes: 18 additions & 18 deletions stdlib/2.7/re.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -34,63 +34,63 @@ def compile(pattern: AnyStr, flags: int = ...) -> Pattern[AnyStr]: ...
def compile(pattern: Pattern[AnyStr], flags: int = ...) -> Pattern[AnyStr]: ...

@overload
def search(pattern: Union[str, unicode], string: AnyStr, flags: int = ...) -> Match[AnyStr]: ...
def search(pattern: Union[bytes, str, unicode], string: AnyStr, flags: int = ...) -> Match[AnyStr]: ...
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every API in this file got amended to add bytes to the union or overload. Beware, it's the pattern that's got the unions -- the search target uses AnyStr and the return type varies with that. But (at least in PY2) the pattern can vary independently.

@overload
def search(pattern: Union[Pattern[str],Pattern[unicode]], string: AnyStr, flags: int = ...) -> Match[AnyStr]: ...
def search(pattern: Union[Pattern[bytes], Pattern[str], Pattern[unicode]], string: AnyStr, flags: int = ...) -> Match[AnyStr]: ...

@overload
def match(pattern: Union[str, unicode], string: AnyStr, flags: int = ...) -> Match[AnyStr]: ...
def match(pattern: Union[bytes, str, unicode], string: AnyStr, flags: int = ...) -> Match[AnyStr]: ...
@overload
def match(pattern: Union[Pattern[str],Pattern[unicode]], string: AnyStr, flags: int = ...) -> Match[AnyStr]: ...
def match(pattern: Union[Pattern[bytes], Pattern[str], Pattern[unicode]], string: AnyStr, flags: int = ...) -> Match[AnyStr]: ...

@overload
def split(pattern: Union[str, unicode], string: AnyStr,
def split(pattern: Union[bytes, str, unicode], string: AnyStr,
maxsplit: int = ..., flags: int = ...) -> List[AnyStr]: ...
@overload
def split(pattern: Union[Pattern[str],Pattern[unicode]], string: AnyStr,
def split(pattern: Union[Pattern[bytes], Pattern[str], Pattern[unicode]], string: AnyStr,
maxsplit: int = ..., flags: int = ...) -> List[AnyStr]: ...

@overload
def findall(pattern: Union[str, unicode], string: AnyStr, flags: int = ...) -> List[Any]: ...
def findall(pattern: Union[bytes, str, unicode], string: AnyStr, flags: int = ...) -> List[Any]: ...
@overload
def findall(pattern: Union[Pattern[str],Pattern[unicode]], string: AnyStr, flags: int = ...) -> List[Any]: ...
def findall(pattern: Union[Pattern[bytes], Pattern[str], Pattern[unicode]], string: AnyStr, flags: int = ...) -> List[Any]: ...

# Return an iterator yielding match objects over all non-overlapping matches
# for the RE pattern in string. The string is scanned left-to-right, and
# matches are returned in the order found. Empty matches are included in the
# result unless they touch the beginning of another match.
@overload
def finditer(pattern: Union[str, unicode], string: AnyStr,
def finditer(pattern: Union[bytes, str, unicode], string: AnyStr,
flags: int = ...) -> Iterator[Match[AnyStr]]: ...
@overload
def finditer(pattern: Union[Pattern[str],Pattern[unicode]], string: AnyStr,
def finditer(pattern: Union[Pattern[bytes], Pattern[str], Pattern[unicode]], string: AnyStr,
flags: int = ...) -> Iterator[Match[AnyStr]]: ...

@overload
def sub(pattern: Union[str, unicode], repl: AnyStr, string: AnyStr, count: int = ...,
def sub(pattern: Union[bytes, str, unicode], repl: AnyStr, string: AnyStr, count: int = ...,
flags: int = ...) -> AnyStr: ...
@overload
def sub(pattern: Union[str, unicode], repl: Callable[[Match[AnyStr]], AnyStr],
def sub(pattern: Union[bytes, str, unicode], repl: Callable[[Match[AnyStr]], AnyStr],
string: AnyStr, count: int = ..., flags: int = ...) -> AnyStr: ...
@overload
def sub(pattern: Union[Pattern[str],Pattern[unicode]], repl: AnyStr, string: AnyStr, count: int = ...,
def sub(pattern: Union[Pattern[bytes], Pattern[str], Pattern[unicode]], repl: AnyStr, string: AnyStr, count: int = ...,
flags: int = ...) -> AnyStr: ...
@overload
def sub(pattern: Union[Pattern[str],Pattern[unicode]], repl: Callable[[Match[AnyStr]], AnyStr],
def sub(pattern: Union[Pattern[bytes], Pattern[str], Pattern[unicode]], repl: Callable[[Match[AnyStr]], AnyStr],
string: AnyStr, count: int = ..., flags: int = ...) -> AnyStr: ...

@overload
def subn(pattern: Union[str, unicode], repl: AnyStr, string: AnyStr, count: int = ...,
def subn(pattern: Union[bytes, str, unicode], repl: AnyStr, string: AnyStr, count: int = ...,
flags: int = ...) -> Tuple[AnyStr, int]: ...
@overload
def subn(pattern: Union[str, unicode], repl: Callable[[Match[AnyStr]], AnyStr],
def subn(pattern: Union[bytes, str, unicode], repl: Callable[[Match[AnyStr]], AnyStr],
string: AnyStr, count: int = ...,
flags: int = ...) -> Tuple[AnyStr, int]: ...
@overload
def subn(pattern: Union[Pattern[str],Pattern[unicode]], repl: AnyStr, string: AnyStr, count: int = ...,
def subn(pattern: Union[Pattern[bytes], Pattern[str], Pattern[unicode]], repl: AnyStr, string: AnyStr, count: int = ...,
flags: int = ...) -> Tuple[AnyStr, int]: ...
@overload
def subn(pattern: Union[Pattern[str],Pattern[unicode]], repl: Callable[[Match[AnyStr]], AnyStr],
def subn(pattern: Union[Pattern[bytes], Pattern[str], Pattern[unicode]], repl: Callable[[Match[AnyStr]], AnyStr],
string: AnyStr, count: int = ...,
flags: int = ...) -> Tuple[AnyStr, int]: ...

Expand Down
4 changes: 2 additions & 2 deletions stdlib/2.7/typing.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ DefaultDict = TypeAlias(object)
Set = TypeAlias(object)

# Predefined type variables.
AnyStr = TypeVar('AnyStr', str, unicode)
AnyStr = TypeVar('AnyStr', bytes, str, unicode)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is key, but also annoying. E.g. this code breaks because of this:

def f(a): # type: (AnyStr) -> AnyStr
    if isinstance(a, str): return '.'
    else: return u'.'

You have to add a third case (elif isinstance(a, bytes): return b'.'). You can'y even collapse this as isinstance(a, (str, bytes)) because the return type (in this example) must match the argument.


# Abstract base classes.

Expand Down Expand Up @@ -262,7 +262,7 @@ class IO(Iterator[AnyStr], Generic[AnyStr]):
# TODO: traceback should be TracebackType but that's defined in types
traceback: Optional[Any]) -> bool: ...

class BinaryIO(IO[str]):
class BinaryIO(IO[bytes]):
# TODO readinto
# TODO read1?
# TODO peek?
Expand Down