Skip to main content
Obfuscation

Python String Encryption: 4 Ways to Hide Strings (Tested)

Pyobfuscate Team··9 min read
Quick Answer

There are two very different goals here. To encrypt string data at runtime, use real crypto — cryptography's Fernet — but the key has to live somewhere. To hide a string literal in your source (an API key, a license value, a URL), encoding tricks like base64 or XOR remove it from a plain-text search but are trivially reversed, because the decode call sits right beside them. The robust option is AST obfuscation, which encrypts literals *and* hides the decoder so the string is gone from the source and from the compiled co_consts. And the limit you can't code around: at runtime the plaintext always exists in memory, so true secrets belong in environment variables, not in code.

"How do I encrypt a string in Python?" gets asked for two completely different reasons, and mixing them up leads to bad advice. Sometimes you want to encrypt *data* — protect a value at rest or in transit. Sometimes you want to hide a *literal* in your source so a customer who opens your .py doesn't immediately see your API key. Those need different tools.

I tested the common approaches — base64, XOR, real Fernet encryption, and our AST obfuscator — on the same secret string, and checked what each one actually achieves. Here's the honest breakdown, with real output.

First, the honest part: encoding is not encryption

The most-copied "encrypt a string" snippets on the internet are actually just encoding. base64 is the classic. It makes your string look like random noise, which stops a casual reader — but there's no key, so anyone reverses it in one call:

SECRET = "sk-live-9f8a7b6c5d"
encoded = base64.b64encode(SECRET.encode()).decode()
# encoded -> 'c2stbGl2ZS05ZjhhN2I2YzVk'  (this is what sits in your file)

base64.b64decode('c2stbGl2ZS05ZjhhN2I2YzVk').decode()
# -> 'sk-live-9f8a7b6c5d'   ...anyone can do this
base64 round-trip (real)

XOR with a key is the next step up, and it has the same fatal flaw: the key is right there in the same file. I XORed the secret with 0x5A and reversed it in one line:

stored = bytes(b ^ 0x5A for b in SECRET.encode())
# stored (hex) -> 29317736332c3f77633c623b6d386c396f3e
bytes(b ^ 0x5A for b in stored).decode()
# -> 'sk-live-9f8a7b6c5d'
XOR round-trip (real)

base64 and XOR are obfuscation, not security. They defeat a grep and accidental exposure, nothing more. Never rely on them to protect anything that actually matters.

Real encryption still needs a key somewhere

If you want genuine encryption, use the cryptography library's Fernet (AES-based, authenticated). It's the right tool for encrypting *data* — a config file, a database field, a message. Real example:

from cryptography.fernet import Fernet
key = Fernet.generate_key()            # e.g. b'sAPGEDzvkTstOcip...'
token = Fernet(key).encrypt(SECRET.encode())
# token -> b'gAAAAABqcKvRvzULfXP9Qd8xJBwrBYQ0...'
Fernet(key).decrypt(token).decode()   # -> 'sk-live-9f8a7b6c5d'
Fernet — real symmetric encryption (real output, trimmed)

This genuinely protects the data — *if* the key is kept separate. And that's the catch for source-hiding: if you're trying to hide a secret that lives inside the script you're shipping, you'd have to ship the key too. An attacker just reads the key and decrypts. Real crypto doesn't solve "a secret baked into distributed code" — it moves the problem to the key.

What actually hides a literal in your source

So which of these removes the secret from your shipped file? I searched the source produced by each method for the plaintext. base64, XOR and Fernet all pass a naive text search (the string is gone), but remember they're reversible. Here's the summary:

MethodGone from a text search?Reversed byGood for
Plain literalNoReading the fileNothing sensitive
base64 / hexYesOne decode callCasual hiding, avoiding accidental leaks
XOR + keyYesTrivial (key in file)Casual hiding
FernetYesAnyone with the key (which ships)Encrypting *data* with an external key
AST obfuscationYes — *and* from co_constsDetermined reverse-engineering onlyProtecting source literals

That last row is the important one. Recall that compiling to bytecode leaves your strings in `co_consts` in plain text — so base64-in-source still leaves the *encoded* blob and the decode call visible, and a plain string leaves the literal itself. Our AST obfuscator encrypts each literal with a key derived from a watermark and hides the decoder, so the string appears nowhere — not in the source, not in the compiled constants. I checked:

SECRET in obfuscated_source   -> False
'sk-live' anywhere in co_consts -> False
obfuscated file still runs      -> prints 'sk-live'  (decrypted at runtime)
after running our obfuscator on `API_KEY = "sk-live-..."` (real)

You can do this yourself in seconds with the free Python Obfuscator — turn on Encrypt and it does exactly this to every string literal in your file, in the browser, nothing uploaded.

The limit you genuinely can't code around

Here's the part no string-hiding trick escapes: at runtime, the plaintext has to exist. However you encode or encrypt a literal in your code, the program must decode it back to the real value to use it — so anyone who can run your code (or attach a debugger, or print it) can recover it. String hiding raises the effort of a static read of your file; it does not make a baked-in secret safe from someone running your program.

For real secrets — API keys, tokens, passwords — don't hide them in code at all. Load them from an environment variable or a secrets manager at runtime, and keep them out of the shipped file entirely. Obfuscate your *logic*; externalize your *secrets*.

Practical recommendation

  • Encrypting data (files, fields, messages)? Use cryptography / Fernet with a key you store separately.
  • Keeping a real secret out of shipped code? Use environment variables or a secrets manager — not any in-code trick.
  • Hiding the string *literals* in source you distribute (endpoints, prompts, license markers, watermarks)? Use AST obfuscation — it encrypts every literal and hides the decoder, clearing them from source and co_consts.
  • Just avoiding a casual grep / shoulder-surf? base64 is fine — as long as you remember it's reversible in one line.

String encryption is one layer of a bigger picture. For the full stack — renaming, string encryption, control-flow flattening and compilation — see how to protect Python source code.

Encrypt your string literals — free

Turn on string encryption in the Python Obfuscator and every literal is encrypted with the decoder hidden. In your browser, nothing uploaded.

Open the Python Obfuscator

Free tools mentioned here

Frequently asked questions

How do I encrypt a string in Python?

For real data encryption, use the cryptography library's Fernet: generate a key, call Fernet(key).encrypt(data), and store the key separately. For hiding a string literal in your source, base64 removes it from a text search but is trivially reversible; AST obfuscation encrypts the literal and hides the decoder so it's gone from both the source and the compiled bytecode.

Is base64 encryption?

No. base64 is encoding, not encryption — there's no key, so anyone can decode it in a single call. It makes a string unreadable at a glance and hides it from a plain text search, but it provides zero real security. Use it only for casual hiding.

Can I hide an API key in a Python script?

You can make it harder to find (obfuscation encrypts the literal so it's not in the source or co_consts), but you can't make it truly safe inside distributed code — at runtime the plaintext must exist in memory. For real secrets, load them from an environment variable or secrets manager instead of embedding them.

What is the difference between encoding, encryption, and obfuscation for strings?

Encoding (base64/hex) changes the representation with no key and is trivially reversible. Encryption (Fernet) uses a key to make data unreadable without it. Obfuscation (AST-level) encrypts string literals in your source and hides the decoder to resist reverse engineering. They solve different problems.

Does Python string obfuscation stop reverse engineering?

It raises the cost, not to infinity. Encrypting literals and hiding the decoder stops casual reading and plain-text searches of your source and bytecode. A determined attacker running your code can still recover values at runtime, so treat it as a strong deterrent, not a guarantee.

Keep reading