PYOTP(1)

๐Ÿ“ฆ version 2.10.0, in arch rolling

๐Ÿ’… PYOTP(1) โ€” PyOTP, aka pyotp, bestie

๐Ÿ’– NAME

pyotp โ€” PyOTP

Okay so PyOTP is a Python library for generating and verifying one-time passwords. It can be used to implement two-factor (2FA) or multi-factor (MFA) authentication methods in web applications and in other systems that require users to log in. Main character energy for your login flow, honestly. โœจ

Open MFA standards are defined in RFC 4226 (https://tools.ietf.org/html/rfc4226) โ€” thatโ€™s HOTP: An HMAC-Based One-Time Password Algorithm โ€” and in RFC 6238 (https://tools.ietf.org/html/rfc6238) โ€” TOTP: Time-Based One-Time Password Algorithm. PyOTP implements server-side support for both of these standards. Client-side support can be enabled by sending authentication codes to users over SMS or email (HOTP) or, for TOTP, by instructing users to use Google Authenticator (https://en.wikipedia.org/wiki/Google_Authenticator), Authy (https://www.authy.com/), or another compatible app. Users can set up auth tokens in their apps easily by using their phone camera to scan otpauth:// (https://github.com/google/google-authenticator/wiki/Key-Uri-Format) QR codes provided by PyOTP. Phone camera goes brrr, secret goes in the app. ๐Ÿ“ฑ๐Ÿฆ‹

Implementers should read and follow the HOTP security requirements (https://tools.ietf.org/html/rfc4226#section-7) and TOTP security considerations (https://tools.ietf.org/html/rfc6238#section-5) sections of the relevant RFCs. At minimum, application implementers should follow this checklist:

  • Ensure transport confidentiality by using HTTPS
  • Ensure HOTP/TOTP secret confidentiality by storing secrets in a controlled access database
  • Deny replay attacks by rejecting one-time passwords that have been used by the client (this requires storing the most recently authenticated timestamp, OTP, or hash of the OTP in your database, and rejecting the OTP when a match is seen)
  • Throttle (rate limit) brute-force attacks against your application's login functionality (see RFC 4226, section 7.3)
  • When implementing a "greenfield" application, consider supporting FIDO U2F (https://en.wikipedia.org/wiki/Universal_2nd_Factor)/WebAuthn (https://www.w3.org/TR/webauthn/) in addition to or instead of HOTP/TOTP. U2F uses asymmetric cryptography to avoid using a shared secret design, which strengthens your MFA solution against server-side attacks. Hardware U2F also sequesters the client secret in a dedicated single-purpose device, which strengthens your clients against client-side attacks. And by automating scoping of credentials to relying party IDs (application origin/domain names), U2F adds protection against phishing attacks. One implementation of FIDO U2F/WebAuthn is PyOTP's sister project, PyWARP (https://github.com/pyauth/pywarp).

We also recommend that implementers read the OWASP Authentication Cheat Sheet (https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Authentication_Cheat_Sheet.md) and NIST SP 800-63-3: Digital Authentication Guideline (https://pages.nist.gov/800-63-3/) for a high level overview of authentication best practices. Security homework but make it slay. ๐Ÿซถ


๐Ÿ“ฑ QUICK OVERVIEW OF USING ONE TIME PASSWORDS ON YOUR PHONE

  • OTPs involve a shared secret, stored both on the phone and the server
  • OTPs can be generated on a phone without internet connectivity
  • OTPs should always be used as a second factor of authentication (if your phone is lost, you account is still secured with a password)
  • Google Authenticator and other OTP client apps allow you to store multiple OTP secrets and provision those using a QR Code

๐Ÿ› ๏ธ INSTALLATION

pip install pyotp

๐Ÿš€ USAGE

โฐ Time-based OTPs

import pyotp
import time

totp = pyotp.TOTP('base32secret3232')
totp.now() # => '492039'

# OTP verified for current time
totp.verify('492039') # => True
time.sleep(30)
totp.verify('492039') # => False

The vibe check: TOTP says โ€œthis code is valid now,โ€ then time moves on and the old code literally stops being invited to the group chat. ๐Ÿ’…

๐Ÿ”ข Counter-based OTPs

import pyotp

hotp = pyotp.HOTP('base32secret3232')
hotp.at(0) # => '260182'
hotp.at(1) # => '055283'
hotp.at(1401) # => '316439'

# OTP verified with a counter
hotp.verify('316439', 1401) # => True
hotp.verify('316439', 1402) # => False

๐Ÿ”‘ Generating a Secret Key

A helper function is provided to generate a 32-character base32 secret, compatible with Google Authenticator and other OTP apps:

pyotp.random_base32()

Some applications want the secret key to be formatted as a hex-encoded string:

pyotp.random_hex()  # returns a 40-character hex-encoded secret

๐Ÿ“ฒ Google Authenticator Compatible

PyOTP works with the Google Authenticator iPhone and Android app, as well as other OTP apps like Authy. PyOTP includes the ability to generate provisioning URIs for use with the QR Code scanner built into these MFA client apps:

pyotp.totp.TOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name='alice@google.com', issuer_name='Secure App')

>>> 'otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App'

pyotp.hotp.HOTP('JBSWY3DPEHPK3PXP').provisioning_uri(name="alice@google.com", issuer_name="Secure App", initial_count=0)

>>> 'otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'

This URL can then be rendered as a QR Code (for example, using https://github.com/soldair/node-qrcode) which can then be scanned and added to the users list of OTP credentials. QR code is the icon, the moment, the whole plot. ๐ŸŽ€

Parsing these URLs is also supported:

pyotp.parse_uri('otpauth://totp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App')

>>> <pyotp.totp.TOTP object at 0xFFFFFFFF>

pyotp.parse_uri('otpauth://hotp/Secure%20App:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=Secure%20App&counter=0'

>>> <pyotp.totp.HOTP object at 0xFFFFFFFF>

๐Ÿงช Working example

Scan the following barcode with your phone's OTP app (e.g. Google Authenticator): [image]

Now run the following and compare the output:

import pyotp
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
print("Current OTP:", totp.now())

๐ŸŽฎ Third-party contributions

The following third-party contributions are not described by a standard, not officially supported, and provided for reference only:

  • pyotp.contrib.Steam(): An implementation of Steam TOTP. Uses the same API as pyotp.TOTP().

For new applications:

๐Ÿท๏ธ Versioning

This package follows the Semantic Versioning 2.0.0 (http://semver.org/) standard. To control changes, it is recommended that application developers pin the package version and manage it using pip-tools (https://github.com/jazzband/pip-tools) or similar. For library developers, pinning the major version is recommended.

[image] <https://github.com/pyauth/pyotp/actions>[image] <https://codecov.io/github/pyauth/pyotp?branch=master>[image] <https://pypi.python.org/pypi/pyotp>[image] <https://pypi.python.org/pypi/pyotp>[image] <https://pyotp.readthedocs.io/>.

๐Ÿซถ INDENT 0.0

Okay so hereโ€™s the API reference chunk that follows:

pyotp.parse_uri(uri: str <https://docs.python.org/3/library/stdtypes.html#str>) -> OTP

Parses the provisioning URI for the OTP; works for either TOTP or HOTP.

See also: https://github.com/google/google-authenticator/wiki/Key-Uri-Format

Parameters

  • uri -- the hotp/totp URI to parse

Returns OTP object

class pyotp.totp.TOTP(s: str <https://docs.python.org/3/library/stdtypes.html#str>, digits: int <https://docs.python.org/3/library/functions.html#int> = 6, digest: Any <https://docs.python.org/3/library/typing.html#typing.Any> = None, name: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, issuer: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, interval: int <https://docs.python.org/3/library/functions.html#int> = 30)

Handler for time-based OTP counters.

at(for_time: int <https://docs.python.org/3/library/functions.html#int> | datetime <https://docs.python.org/3/library/datetime.html#datetime.datetime>, counter_offset: int <https://docs.python.org/3/library/functions.html#int> = 0) -> str <https://docs.python.org/3/library/stdtypes.html#str>

Accepts either a Unix timestamp integer or a datetime object.

To get the time until the next timecode change (seconds until the current OTP expires), use this instead:

totp = pyotp.TOTP(...)
time_remaining = totp.interval - datetime.datetime.now().timestamp() % totp.interval

Parameters

  • for_time -- the time to generate an OTP for
  • counter_offset -- the amount of ticks to add to the time counter

Returns OTP value

now() -> str <https://docs.python.org/3/library/stdtypes.html#str>

Generate the current time OTP

Returns OTP value

provisioning_uri(name: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, issuer_name: str <https://docs.python.org/3/library/stdtypes.html#str> | None <https://docs.python.org/3/library/constants.html#None> = None, **kwargs) -> str <https://docs.python.org/3/library/stdtypes.html#str>

Returns the provisioning URI for the OTP. This can then be encoded in a QR Code and used to provision an OTP app like Google Authenticator.

See also: https://github.com/google/google-authenticator/wiki/Key-Uri-Format ๐Ÿซถ

๐Ÿ’– bestie that link is the literal blueprint for how the whole URI thing is supposed to look โ€” keep it bookmarked, iykyk.

โœจ timecode(for_time: datetime) -> int

okay so this one takes either a timezone naive datetime (for_time.tzinfo is None) or a timezone aware datetime as its argument, and hands you back the matching counter value โ€” the timecode. that's it, that's the whole vibe. โฐ

โœจ verify(otp: str, for_time: datetime | None = None, valid_window: int = 0) -> bool

Checks the OTP you passed in against the current time OTP. slay. ๐Ÿ”ฅ

Parameters

  • otp โ€” the OTP to check against
  • for_time โ€” Time to check OTP at (defaults to now)
  • valid_window โ€” extends the validity to this many counter ticks before and after the current one

Returns

True if verification succeeded, False otherwise ๐Ÿซถ


๐Ÿ‘‘ class pyotp.hotp.HOTP(s: str, digits: int = 6, digest: Any = None, name: str | None = None, issuer: str | None = None, initial_count: int = 0)

Handler for HMAC-based OTP counters. counter-core, no clock, just vibes ๐Ÿ’…

๐ŸŒธ at(count: int) -> str

Generates the OTP for the given count.

Parameters

  • count โ€” the OTP HMAC counter

Returns

OTP โœจ

๐ŸŒธ provisioning_uri(name: str | None = None, initial_count: int | None = None, issuer_name: str | None = None, **kwargs) -> str

Returns the provisioning URI for the OTP. This can then be encoded in a QR Code and used to provision an OTP app like Google Authenticator.

See also: https://github.com/google/google-authenticator/wiki/Key-Uri-Format

Parameters

  • name โ€” name of the user account
  • initial_count โ€” starting HMAC counter value, defaults to 0
  • issuer_name โ€” the name of the OTP issuer; this will be the organization title of the OTP entry in Authenticator

Returns

provisioning URI ๐ŸŽ€

๐ŸŒธ verify(otp: str, counter: int) -> bool

Verifies the OTP passed in against the current counter OTP.

Parameters

  • otp โ€” the OTP to check against
  • counter โ€” the OTP HMAC counter

๐Ÿฆ‹ pyotp.utils.build_uri(secret: str, name: str, initial_count: int | None = None, issuer: str | None = None, algorithm: str | None = None, digits: int | None = None, period: int | None = None, **kwargs) -> str

Returns the provisioning URI for the OTP; works for either TOTP or HOTP. โœจ

This can then be encoded in a QR Code and used to provision the Google Authenticator app.

For module-internal use. (translation: it's the plumbing, bestie โ€” it's not really for you to call directly, but here's the tea anyway ๐Ÿซ–)

See also: https://github.com/google/google-authenticator/wiki/Key-Uri-Format

Parameters

  • secret โ€” the hotp/totp secret used to generate the URI
  • name โ€” name of the account
  • initial_count โ€” starting counter value, defaults to None. If none, the OTP type will be assumed as TOTP.
  • issuer โ€” the name of the OTP issuer; this will be the organization title of the OTP entry in Authenticator
  • algorithm โ€” the algorithm used in the OTP generation.
  • digits โ€” the length of the OTP generated code.
  • period โ€” the number of seconds the OTP generator is set to expire every code.
  • kwargs โ€” other query string parameters to include in the URI

Returns

provisioning uri ๐Ÿ’œ


๐Ÿ’€ pyotp.utils.strings_equal(s1: str, s2: str) -> bool

Timing-attack resistant string comparison. ๐Ÿ›ก๏ธ

So like, normal comparison using == will short-circuit on the first mismatching character. This avoids that by scanning the whole string โ€” though we still reveal to a timing attack whether the strings are the same length. (no cap, that's literally what it says โ€” length is still leaked, it's not perfect, but it's way better than ==) ๐Ÿ˜ญ


๐ŸŽฎ class pyotp.contrib.steam.Steam(s: str, name: str | None = None, issuer: str | None = None, interval: int = 30, digits: int = 5)

Steam's custom TOTP. Subclass of pyotp.totp.TOTP. gamer girl OTP era ๐Ÿ•น๏ธ๐Ÿ’…

๐ŸŽฎ generate_otp(input: int) -> str

Parameters

  • input โ€” the HMAC counter value to use as the OTP input. Usually either the counter, or the computed integer based on the Unix timestamp

๐ŸŽ€ RELEASE NOTES ๐ŸŽ€

๐ŸŒˆ CHANGES FOR V2.10.0 (2026-06-13)

  • Fix parse_uri mis-parsing of encoded colon in issuer/account name (#187)
  • Simplified and improved generate_otp() performance in Steam class (#179)
  • Check digest function to prevent error on OTP Generation (#170)
  • Ignore non-standard otpauth parameters
  • use strings as immutable default arguments (#162)

๐ŸŒˆ CHANGES FOR V2.9.0 (2023-07-27)

  • Add parse_uri() support for Steam TOTP (#153)
  • Test and documentation improvements

๐ŸŒˆ CHANGES FOR V2.8.0 (2022-12-13)

  • Modify OTP generation to run in constant time (#148)
  • Documentation improvements
  • Drop Python 3.6 support; introduce Python 3.11 support

๐ŸŒˆ CHANGES FOR V2.7.0 (2022-09-11)

  • Support Steam TOTP (#142)
  • Build, test, and documentation updates

๐ŸŒˆ CHANGES FOR V2.6.0 (2021-02-04)

  • Raise default and minimum base32 secret length to 32, and hex secret length to 40 (160 bits as recommended by the RFC) (#115).
  • Fix issue where provisioning_uri would return invalid results after calling verify() (#115).

๐ŸŒˆ CHANGES FOR V2.5.1 (2021-01-29)

  • parse_uri accepts and ignores optional image parameter (#114)

๐ŸŒˆ CHANGES FOR V2.5.0 (2021-01-29)

  • Add optional image parameter to provisioning_uri (#113)
  • Support for 7-digit codes in parse_uri (#111)
  • Raise default and minimum base32 secret length to 26

๐ŸŒˆ CHANGES FOR V2.4.1 (2020-10-16)

  • parse_uri: Fix handling of period, counter (#108)
  • Add support for timezone aware datetime as argument to TOTP.timecode() (#107)

๐ŸŒˆ CHANGES FOR V2.4.0 (2020-07-29)

  • Fix data type for at(for_time) (#85)
  • Add support for parsing provisioning URIs (#84)
  • Raise error when trying to generate secret that is too short (The secret must be at least 128 bits)
  • Add random_hex function (#82)

๐ŸŒˆ CHANGES FOR V2.3.0 (2019-07-26)

  • Fix comparison behavior on Python 2.7

๐ŸŒˆ CHANGES FOR V2.2.8 (2019-07-26)

  • Fix comparison of unicode chars (#78)
  • Minor documentation and test fixes

๐ŸŒˆ CHANGES FOR V2.2.7 (2018-11-05)

  • Have random_base32() use 'secrets' as rand source (#66)
  • Documentation: Add security considerations, minimal security checklist, other improvements
  • Update setup.py to reference correct license

๐ŸŒˆ CHANGES FOR V2.2.6 (2017-06-10)

  • Fix tests wrt double-quoting in provisioning URIs

๐ŸŒˆ CHANGES FOR V2.2.5 (2017-06-03)

  • Quote issuer QS parameter in provisioning_uri. Fixes #47.
  • Raise an exception if a negative integer is passed to at() (#41).
  • Documentation and release infrastructure improvements.

๐ŸŒˆ CHANGES FOR V2.2.4 (2017-01-04)

  • Restore Python 2.6 compatibility (however, Python 2.6 is not supported)
  • Documentation and test improvements
  • Fix release infra script, part 2

โœจ CHANGES FOR V2.2.3 (2017-01-04) โœจ

  • Brought back Python 2.6 compatibility โ€” but bestie, read the fine print: Python 2.6 is not supported ๐Ÿ’€ like it's compatible but it's not invited to the party, iykyk.
  • Docs and test glow-ups ๐Ÿ“šโœจ
  • Fixed the release infra script โ€” the behind-the-scenes machinery was being lowkey messy and now it's not ๐Ÿ› ๏ธ

๐ŸŒธ CHANGES FOR V2.2.2 (2017-01-04) ๐ŸŒธ

  • Restored Python 2.6 compatibility again (and again: Python 2.6 is not supported, it's giving "we see you but no" ๐Ÿ™ˆ)
  • Documentation and test improvements, the usual lil spruse-up ๐ŸŽ€

๐Ÿฆ‹ CHANGES FOR V2.2.1 (2016-08-30) ๐Ÿฆ‹

  • Ditched python-future entirely โ€” it had subdependencies that were limiting compatibility, like a friend-of-a-friend who ruins the group chat vibe (#34) ๐Ÿ˜ญ
  • Made the test suite actually pass on 32-bit platforms (#30) โ€” 32-bit girlies we see you ๐Ÿซถ
  • Timing attack resistance fix: don't reveal the string length to an attacker. Shoutout to Eeo Jun for this one (#28) ๐Ÿ”ฅ that's main character security energy fr fr.
  • algorithm, digits, and period parameters are now supported in provisioning_uri. Thanks to Dionisio E Alonso (#33) ๐Ÿ‘‘
  • Minor style and packaging infrastructure fixes โ€” small but slay โœจ

๐Ÿ’œ CHANGES FOR V2.2.0 (2016-08-30) ๐Ÿ’œ

  • Literally just see v2.2.1, that's the whole thing ๐Ÿซ 

๐ŸŽ€ Version 2.1.0 (2016-05-02) ๐ŸŽ€

  • Added extended range support to TOTP.verify. Thanks to Zeev Rotshtein (PR #19) ๐Ÿ’…
  • Now handles missing padding of the encoded secret. Thanks to Kun Yan (#20) ๐Ÿงš
  • Miscellaneous fixes, the little cleanup crumbs ๐ŸŒธ

โญ Version 2.0.1 (2015-09-28) โญ

  • Fixed a packaging issue in v2.0.0 that was blocking installation with easy_install โ€” iconic that it's fixed, tragic that it happened ๐Ÿ“ฆ๐Ÿ˜ญ

๐ŸŒˆ Version 2.0.0 (2015-08-22) ๐ŸŒˆ

  • Big glow-up: the pyotp.HOTP.at(), pyotp.TOTP.at(), and pyotp.TOTP.now() methods now return strings instead of integers. Thanks to Rohan Dhaimade (PR #16) ๐ŸŽ‰ not me being emotionally attached to a return type changeโ€”

๐Ÿ’– Version 1.4.2 (2015-07-21) ๐Ÿ’–

  • Started tracking changes in the change log (hi, that's this thing you're reading bestie ๐Ÿ‘‹)
  • Updated the documentation ๐Ÿ“
  • Introduced Travis CI integration โ€” automation era begins ๐Ÿค–โœจ

๐Ÿ“ Version 1.3.1 (2012-02-29) ๐Ÿ“

  • Initial release. The origin story. The big bang. Periodt ๐Ÿ’ฅ๐Ÿ‘‘

๐Ÿ‘‘ Author

PyOTP contributors ๐Ÿซถ

PyOTP contributors ๐Ÿ“œ


okay that's the whole page and honestly? we ate ๐Ÿ’… the source is this page's own version info, dated Jun 16, 2026, and it's filed under PYOTP(1) โ€” so go forth, verify those one-time passwords, and remember: don't reveal your string length to attackers, that's not the vibe ๐ŸŒธโœจ๐Ÿ’œ

๐Ÿ“ฆ where this page comes from