Skip to content

'NoneType' object is not subscriptable #33

@g0tmi1k

Description

@g0tmi1k

Error

From 2025-10-24, I've noticed I was getting the following error:

[i] Logging into Substack...
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): substack.com:443
DEBUG:urllib3.connectionpool:https://substack.com:443 "GET /api/v1/user/profile/self HTTP/1.1" 200 None
[-] An unexpected error occurred: 'NoneType' object is not subscriptable

Code

This is my code which is in use:

print("[i] Logging into Substack...")
try:
    api = Api(
        debug=logging.getLogger().isEnabledFor(logging.DEBUG),
        publication_url=os.getenv("PUBLICATION_URL"),
        cookies_path=cookie,
        email=email,
        password=password
    )
    print("[+] Logged into Substack!")
except Exception as e:
    print(f"[-] An unexpected error occurred: {e}")
    sys.exit(1)

API (Today)

Digging into the API response as of today (2025-12-18):

$ curl \
    --silent \
    --header 'Cookie: substack.sid=[REMOVED]' \
   'https://substack.com/api/v1/user/profile/self' \
  | jq '.. | objects | keys'
[
  "bio",
  "can_dm",
  "facebookAccount",
  "followerCount",
  "followsViewer",
  "handle",
  "hasActivity",
  "hasGuestPost",
  "hasHiddenPublicationUsers",
  "hasHiddenSubscriptions",
  "hasLikes",
  "id",
  "isFollowing",
  "isSubscribed",
  "lists",
  "max_pub_tier",
  "name",
  "photo_url",
  "previousSlug",
  "previous_name",
  "primaryPublicationSubscriptionState",
  "profile_disabled",
  "profile_set_up_at",
  "publicationUsers",
  "reader_installed_at",
  "slug",
  "status",
  "subscriberCount",
  "subscriberCountNumber",
  "subscriberCountString",
  "subscribesToViewerSubdomain",
  "subscriptions",
  "subscriptionsTruncated",
  "theme",
  "tos_accepted_at",
  "twitterAccount",
  "userLinks",
  "visibleSubscriptionsCount"
]
[
  "id",
  "label",
  "type",
  "url",
  "value"
]
[
  "id",
  "label",
  "type",
  "url",
  "value"
]
[
  "id",
  "label",
  "type",
  "url",
  "value"
]
[]
[
  "badge",
  "bestsellerTier",
  "leaderboard",
  "paidPublicationIds",
  "subscriber",
  "subscriberTier",
  "vip"
]
$

API (Previously)

Looking at NHagar/substack_ap (WayBackMachine), was able to see a able to see the historical version of the API response:

$ cat /tmp/usage_walkthrough.json | jq 'keys'
[
  "bestseller_badge_disabled",
  "bestseller_tier",
  "bio",
  "followsViewer",
  "handle",
  "hasActivity",
  "hasGuestPost",
  "hasHiddenPublicationUsers",
  "hasLikes",
  "id",
  "isFollowing",
  "isPersonalEligible",
  "isSubscribed",
  "lists",
  "max_pub_tier",
  "name",
  "photo_url",
  "previousSlug",
  "previous_name",
  "primaryPublication",
  "profile_disabled",
  "profile_set_up_at",
  "publicationUsers",
  "rough_num_free_subscribers",
  "rough_num_free_subscribers_int",
  "slug",
  "subdomainUrl",
  "subscriberCount",
  "subscriberCountNumber",
  "subscriberCountString",
  "subscriptions",
  "subscriptionsTruncated",
  "tos_accepted_at",
  "userLinks",
  "visibleSubscriptionsCount"
]
$

Converted JSON: usage_walkthrough.json

Compare APIs

$ cat /tmp/usage_walkthrough.json| jq 'keys' > usage_walkthrough-keys
$ 
$ curl \
    --silent \
    --header 'Cookie: substack.sid=[REMOVED]' \
   'https://substack.com/api/v1/user/profile/self' | jq 'key > self-keys
$ 
$ diff self-keys usage_walkthrough-keys
1a2,3
>   "bestseller_badge_disabled",
>   "bestseller_tier",
3,5d4
<   "can_dm",
<   "facebookAccount",
<   "followerCount",
11d9
<   "hasHiddenSubscriptions",
14a13
>   "isPersonalEligible",
22c21
<   "primaryPublicationSubscriptionState",
---
>   "primaryPublication",
26c25,26
<   "reader_installed_at",
---
>   "rough_num_free_subscribers",
>   "rough_num_free_subscribers_int",
28c28
<   "status",
---
>   "subdomainUrl",
32d31
<   "subscribesToViewerSubdomain",
35d33
<   "theme",
37d34
<   "twitterAccount",
$

Following the error

Looking for /api/v1/user/profile/self:

$ git clone https://github.com/ma2za/python-substack
[...]
$
$ cd ./python-substack/
$
$ grep -Rn '/api/v1/user/profile/self'
$
$ grep -Rn '/self'
./substack/api.py:234:        response = self._session.get(f"{self.base_url}/user/profile/self")
$
$ grep -Rn -C 5 '/self'
./substack/api.py-229-
./substack/api.py-230-    def get_user_profile(self):
./substack/api.py-231-        """
./substack/api.py-232-        Gets the users profile
./substack/api.py-233-        """
./substack/api.py:234:        response = self._session.get(f"{self.base_url}/user/profile/self")
./substack/api.py-235-
./substack/api.py-236-        return Api._handle_response(response=response)
./substack/api.py-237-
./substack/api.py-238-    def get_user_settings(self):
./substack/api.py-239-        """

Can see that ./substack/api.py has get_user_profile().


$ grep -n get_user_profile ./substack/api.py | wc -l
       4
$
$ grep -n get_user_profile ./substack/api.py
179:        profile = self.get_user_profile()
205:        profile = self.get_user_profile()
218:        profile = self.get_user_profile()
230:    def get_user_profile(self):
$

And its only used called in 3 places.

$ grep -nB 6 self.get_user_profile ./substack/api.py
173-    def get_user_id(self):
174-        """
175-
176-        Returns:
177-
178-        """
179:        profile = self.get_user_profile()
--
199-
200-    def get_user_primary_publication(self):
201-        """
202-        Gets the users primary publication
203-        """
204-
205:        profile = self.get_user_profile()
--
212-
213-    def get_user_publications(self):
214-        """
215-        Gets the users publications
216-        """
217-
218:        profile = self.get_user_profile()
$ 
  • get_user_id()
  • get_user_primary_publication()
  • get_user_publications()

get_user_id()

$ grep get_user_id ./substack/api.py
    def get_user_id(self):
$

...As our code doesn't use get_user_id, its not going to be this

get_user_primary_publication() / get_user_publications()

$ grep self.get_user_primary_publication ./substack/api.py
            user_publication = self.get_user_primary_publication()
$ 
$ grep self.get_user_publications ./substack/api.py
            user_publications = self.get_user_publications()
$ 

So let's see where these are being called:

class Api:
[...]
    def __init__(
[...]
        # if the user provided a publication url, then use that
        if publication_url:
[...]
            user_publications = self.get_user_publications()
[...]
        else:
            # get the users primary publication
            user_publication = self.get_user_primary_publication()

Bingo! This starts to match up whats in the code (right at the top)
So using Api(), if publication_url is set, get_user_publications() is called, Otherwise its get_user_primary_publication()
For what its worth, publication_url is set for me

get_user_publications()

Looking at the function:

    def get_user_publications(self):
        """
        Gets the users publications
        """

        profile = self.get_user_profile()

        # Loop through users "publicationUsers" list, and return a list
        # of dictionaries of "name", and "subdomain", and "id"
        user_publications = []
        for publication in profile["publicationUsers"]:
[...]

Its expecting publicationUsers in the API response back....
It USED to be there, but its not anymore!

get_user_primary_publication()

Looking at the function:

    def get_user_primary_publication(self):
        """
        Gets the users primary publication
        """

        profile = self.get_user_profile()
        primary_publication = profile["primaryPublication"]
[...]

Its expecting primaryPublication in the API response back....
Again It USED to be there, but its not anymore!

This may start to explain why its failing....

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions