(type-safety)= # PDF type safety with explicit mode > Explicit is better than implicit. > > — Tim Peters, *The Zen of Python* ({pep}`20`) pikepdf is moving from *implicit* conversion, where values read from a PDF are quietly turned into Python types, to *explicit* conversion, where they keep their PDF type until you ask for a Python value. This page explains why, shows what the difference looks like in real code, and sets out the timetable. For the reference details of each API mentioned here, see {ref}`Explicit conversion mode ` in the object model topic. ## PDFs are weakly typed Every object in a PDF has a type: boolean, integer, real, string, name, array, dictionary, stream, or null. The file's syntax makes the type of each value unambiguous: `1024` is an integer, `(1024)` is a string, `/1024` is a name. The file format does not say what type a value *should* have. That knowledge lives in the prose of the PDF specification, table by table: an image's `/Width` must be an integer, a page's `/MediaBox` must be an array of four numbers, and so on. There is no schema in the file and nothing that enforces one. A program that reads a PDF only finds out whether `/Width` is an integer when it looks. pikepdf's development began in 2016, when Python type checking was still in its infancy. Type hints had been standardized only the year before, mypy was experimental, and most Python libraries, this one included, were designed around duck typing. The natural choice then was to convert each PDF scalar to the nearest Python type and let Python's dynamism do the rest. That choice has aged poorly, for the reasons below. ## PDF types are not Python types The "nearest Python type" for a PDF scalar is close to it, but not the same thing. **A PDF integer is not a Python `int`.** Python integers are unbounded. PDF integers are bounded; qpdf, and so pikepdf, stores them as signed 64-bit values. A PDF that contains an integer too large for that range cannot be read as an integer at all: ```{eval-rst} .. doctest:: >>> pikepdf.Object.parse(b'99999999999999999999') Traceback (most recent call last): ... pikepdf._core.PdfError: parsed object (offset 0): treating object as null because of error during parsing: overflow/underflow converting 99999999999999999999 to 64-bit integer ``` Going the other way, a Python `int` outside that range cannot be stored in a PDF: ```{eval-rst} .. doctest:: >>> pikepdf.Array([2**64]) Traceback (most recent call last): ... OverflowError: value is out of range for a 64-bit PDF integer ``` **A PDF real is not a Python `float` or `Decimal`.** PDF reals are written as plain decimal numbers with no exponent: `0.00001` is a real, and `1e-5` is not a number at all. A Python `float` is a binary approximation that may not equal the decimal digits in the file. {class}`~decimal.Decimal`, which pikepdf uses to preserve those digits, can hold values such as `Decimal('1E-20')` that no PDF real can express. When pikepdf writes such a value it has to round it to a fixed number of decimal places, and a small enough value becomes `0`. ```{eval-rst} .. doctest:: >>> from decimal import Decimal >>> pikepdf.Array([Decimal('1E-20')]).unparse() b'[ 0 ]' ``` **A PDF string is not a Python `str`.** A PDF string is a sequence of bytes. Some strings hold *text*, such as a document title or a form field value, encoded in PDFDocEncoding or UTF-16 (or, in PDF 2.0, UTF-8). For those, treating the string as a `str` is the right thing to do. Other strings hold arbitrary binary data, such as a document's `/ID`, and have no meaningful text decoding. A {class}`pikepdf.String` gives you either view: `str()` decodes it as text and `bytes()` returns the raw data. ## Well-formed is not the same as valid A PDF can be *well-formed*, meaning its syntax parses, and still be *invalid*, meaning its contents break the rules of the specification. Consider an image XObject whose `/Width`, which the specification requires to be an integer, was written by some buggy software as a string, a real, or even a name: ```text << /Type /XObject /Subtype /Image /Width 1024 ... >> % valid << /Type /XObject /Subtype /Image /Width (1024) ... >> % a string << /Type /XObject /Subtype /Image /Width 1024.0 ... >> % a real << /Type /XObject /Subtype /Image /Width /1024 ... >> % a name ``` All four are perfectly good PDF syntax. qpdf is very good at repairing PDFs that are *not* well-formed: broken cross-reference tables, wrong stream lengths, missing `endobj` keywords. But it cannot repair validity. It has no way to know that `(1024)` was meant to be `1024`, or whether a `/Width` of `/1024` is a typo or garbage. In practice, many PDFs in the wild have problems with their internal typing. pikepdf is a low-level tool, and reading, inspecting and fixing invalid PDFs is squarely within its job. So it must report what is actually in the file, not what ought to be there, and code that uses it needs a clean way to tell the two apart. ## The implicit way In *implicit* conversion mode, the current default, pikepdf converts PDF scalars to Python types as you read them: integers become `int`, booleans become `bool`, reals become `Decimal`, and null becomes `None`. Strings, names, arrays and dictionaries stay as pikepdf objects. For a valid PDF this works well and reads naturally. Here is how to find the width of the image named `/Im0` on a page: ```python width = page.Resources.XObject.Im0.Width ``` This gives `1024` for a valid file. For the invalid ones above, it gives `pikepdf.String("1024")`, `Decimal('1024.0')` or `pikepdf.Name("/1024")`, with no error. The bug appears later, somewhere else: `range(width)` raises a `TypeError`, `width * height` quietly produces a `Decimal`, and a `/Width` of `true` passes `isinstance(width, int)` because `bool` is a subclass of `int`. The one-liner also assumes a lot about the file. Robust code has to ask: - What if the page has no `/Resources` dictionary? - What if the resources have no `/XObject` dictionary? - What if there is no `/Im0`? - What if `/Im0` is not a stream? - What if it is a stream, but not an image (a form XObject, say)? - What if the image has no `/Width`? - What if `/Width` has the wrong type? Answering all of those in implicit mode looks something like this: ```{eval-rst} .. testsetup:: from pikepdf import Pdf, Dictionary, Name, NamePath, Stream, String pdf = Pdf.new() pdf.add_blank_page() page = pdf.pages[0] image = Stream( pdf, b'\x00' * 1024, Type=Name.XObject, Subtype=Name.Image, Width=1024, Height=1, BitsPerComponent=8, ColorSpace=Name.DeviceGray, ) page.Resources = Dictionary(XObject=Dictionary(Im0=image)) .. testcode:: def image_width(page, name): resources = page.obj.get(Name.Resources) if not isinstance(resources, Dictionary): return None xobjects = resources.get(Name.XObject) if not isinstance(xobjects, Dictionary): return None image = xobjects.get(name) if not isinstance(image, Stream): return None if image.get(Name.Subtype) != Name.Image: return None width = image.get(Name.Width) if isinstance(width, bool) or not isinstance(width, int): return None return width print(image_width(page, Name.Im0)) .. testoutput:: 1024 ``` All of that just to find out how wide an image is. Every check is needed, and forgetting any one of them is a latent bug that only an unusual file will reveal. Note the `isinstance(width, bool)` test, which is easy to forget. Type checkers cannot help, either. pikepdf's type hints say that `image.Width` returns a `pikepdf.Object`, since that is the only honest answer when the value could be anything. In implicit mode it may actually be an `int`, a `Decimal`, `None`, or an object, so a type checker that believes the hints is wrong about common pikepdf code, and cannot spot the bugs above. ## The explicit way In *explicit* conversion mode, reading a value from a PDF always gives you a pikepdf object: {class}`pikepdf.Integer`, {class}`pikepdf.Boolean`, {class}`pikepdf.Real`, `pikepdf.String`, and so on. You convert it to Python when you need a Python value, and the conversion checks the type for you. ### Getting a value or a default When a missing or wrong-typed value should just be treated as absent, {attr}`Page.resources `, a {class}`~pikepdf.NamePath` and a typed getter such as {meth}`~pikepdf.Object.get_int` answer the questions in the list above in one line: ```{eval-rst} .. doctest:: >>> page.resources.get_int(NamePath.XObject.Im0.Width) 1024 ``` `page.resources` finds the page's resources even when they are inherited from the page tree, and gives an empty dictionary if the page has none. From there, if any step of the path is missing or is not a dictionary, or if the final value is not an integer, you get the default, which is `None` unless you give one: ```{eval-rst} .. doctest:: >>> image.Width = String('1024') >>> page.resources.get_int(NamePath.XObject.Im0.Width) is None True >>> page.resources.get_int(NamePath.XObject.Missing.Width, 0) 0 ``` The typed getters work the same in either conversion mode, so this line is safe to use today. Two questions are left over. A page whose `/Resources` is not a dictionary at all makes `page.resources` raise `TypeError`. And the line does not check the image's `/Subtype`; add a check of `NamePath.XObject.Im0.Subtype` if that matters to you. ### Converting a value already in hand The typed getters look a value up by key. For a value you already hold -- an array element, a content stream operand, a value from `.items()`, or a parameter of a function that may be called from either kind of code -- the module-level functions {func}`pikepdf.as_int`, {func}`pikepdf.as_bool`, {func}`pikepdf.as_float`, {func}`pikepdf.as_decimal`, {func}`pikepdf.as_dict`, {func}`pikepdf.as_list`, {func}`pikepdf.as_str` and {func}`pikepdf.as_bytes` apply the same rules. They accept the pikepdf object explicit mode gives you and the native value implicit mode would have given you instead, so the answer does not depend on the mode: ```{eval-rst} .. doctest:: >>> box = pikepdf.Array([0, 0, pikepdf.Real('612.5'), True]) >>> [pikepdf.as_float(v) for v in box] [0.0, 0.0, 612.5, None] >>> with pikepdf.explicit_conversion(): ... [pikepdf.as_float(v) for v in box] [0.0, 0.0, 612.5, None] ``` A native value counts only if it is the type implicit mode would have produced for the PDF type asked for, so `pikepdf.as_int(True)` gives the default: a Boolean is not an Integer, even though Python's `bool` is a subclass of `int`. Unlike {func}`pikepdf.unbox`, which passes a value of an unexpected type through, these return the default for it. ### Retrieving or failing When a malformed file should be an error, explicit mode and the `as_*` accessors let you retrieve a value or fail with a precise message: ```{eval-rst} .. doctest:: >>> with pikepdf.explicit_conversion(): ... image = page.resources[NamePath.XObject.Im0] ... image.Width.as_int() Traceback (most recent call last): ... TypeError: Expected integer, got string >>> with pikepdf.explicit_conversion(): ... page.resources[NamePath.XObject.Im1] Traceback (most recent call last): ... KeyError: 'Key /Im1 not found; traversed NamePath.XObject' ``` A bad file now fails at the point where the bad value was read, with a message that says what was wrong, instead of somewhere downstream. ### Repairing Because pikepdf is often used to fix broken files, the accessors and getters accept `coerce=True`, which also accepts values stored with a "nearby" type: for `as_int`, a real (truncated toward zero) or a numeric string. That makes a repair straightforward: ```{eval-rst} .. doctest:: >>> with pikepdf.explicit_conversion(): ... width = image.get_int(Name.Width, coerce=True) ... if width is not None and not isinstance(image.Width, pikepdf.Integer): ... image.Width = width # write back a proper integer ... image.Width pikepdf.Integer(1024) ``` A value that cannot be coerced, such as the name `/1024`, still returns the default (or raises `TypeError` if no default is given), so `coerce=True` widens what is accepted without hiding garbage. :::{tip} These examples use image dimensions because they are easy to follow. For real work with images, use {meth}`pikepdf.Page.get_images` and {class}`pikepdf.PdfImage`, which walk the resources for you (including images nested inside form XObjects) and raise `TypeError` for a malformed image rather than returning a wrong answer: ```{eval-rst} .. doctest:: >>> image.Width = Name('/1024') >>> try: ... pikepdf.PdfImage(image).width ... except TypeError as e: ... print(e) Image /Width has a value of the wrong type: pikepdf.Name("/1024") ``` ::: ## Implicit vs explicit | Implicit mode | Explicit mode | | --- | --- | | PDF scalars are silently converted to Python types. | PDF scalars stay PDF objects until you convert them. | | A wrong type in an invalid PDF passes through unnoticed and causes bugs elsewhere, so safe access needs complicated checks. | A wrong type is caught when the value is unboxed, and the `as_*` accessors and `get_*` getters make wrong types easy to handle. | | Type checkers cannot tell what common pikepdf code returns. | Type checkers can read pikepdf code and spot bugs early. | | Terse on the happy path. | A little more verbose on the happy path. | | Verbose on the failure path. | Concise on the failure path. | Explicit mode does not make everything more verbose. Arithmetic and comparisons on `Integer` and `Real` work directly and give native Python results, so `mediabox[2] - mediabox[0] > 100` needs no change, and raises `TypeError` instead of giving a wrong answer if a coordinate is not a number. See {ref}`Explicit conversion mode ` for the full behavior. ## Three ways to use explicit mode **Per document.** Pass `conversion_mode='explicit'` when opening or creating a PDF. All objects read from that document are explicit, whichever thread reads them, and no other document is affected. This is the right choice for a library that handles PDFs on behalf of an application. ```python pdf = pikepdf.open('input.pdf', conversion_mode='explicit') new_pdf = pikepdf.new(conversion_mode='explicit') pdf.conversion_mode = 'implicit' # can be changed later ``` **Globally.** Set the default for the whole process. This suits applications, scripts and test suites; a library should not change a global setting that belongs to its host application. ```python pikepdf.set_object_conversion_mode('explicit') ``` **For a block of code.** The {func}`pikepdf.explicit_conversion` and {func}`pikepdf.implicit_conversion` context managers change the mode for the current thread for the duration of a `with` block. They are useful for migrating a code base one function at a time, or for calling old code that still expects implicit mode. ```python with pikepdf.explicit_conversion(): width = image.Width.as_int() with pikepdf.implicit_conversion(): legacy_function(pdf) ``` When more than one applies, the context manager wins, then the per-document mode, then the global default. If your code must work in both modes during a transition, use the mode-independent APIs: the `get_*` typed getters, {meth}`~pikepdf.Object.get_raw`, the `as_*` accessors, and {func}`pikepdf.unbox`. The {ref}`migration checklist ` lists the changes that switching modes can silently break. (type-safety-deprecation)= ## Deprecation schedule Implicit mode is being phased out over three major releases: | Version | Change | | --- | --- | | v11 | Implicit mode remains the default, but pikepdf issues a `DeprecationWarning` whenever an implicit conversion happens, with advice on how to fix the code that triggered it. | | v12 | Explicit mode becomes the default. Implicit mode is still available, but you must opt in to it. | | v13 | Implicit mode is removed. | You do not need to wait for v11. The APIs described on this page are available now, and code written with them works in both modes. A good first step is to run your test suite with `pikepdf.set_object_conversion_mode('explicit')` in effect and see what breaks.