Skip to content
Open
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
2 changes: 1 addition & 1 deletion twikit/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ async def request(
response_data = response.text

if isinstance(response_data, dict) and 'errors' in response_data:
error_code = response_data['errors'][0]['code']
error_code = response_data['errors'][0].get('code')
error_message = response_data['errors'][0].get('message')
if error_code in (37, 64):
# Account suspended
Expand Down
62 changes: 31 additions & 31 deletions twikit/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,40 +88,40 @@ class User:

def __init__(self, client: Client, data: dict) -> None:
self._client = client
legacy = data['legacy']
legacy = data.get('legacy', {})

self.id: str = data['rest_id']
self.created_at: str = legacy['created_at']
self.name: str = legacy['name']
self.screen_name: str = legacy['screen_name']
self.profile_image_url: str = legacy['profile_image_url_https']
self.id: str = data.get('rest_id', '')
self.created_at: str = legacy.get('created_at', '')
self.name: str = legacy.get('name', '')
self.screen_name: str = legacy.get('screen_name', '')
self.profile_image_url: str = legacy.get('profile_image_url_https', '')
self.profile_banner_url: str = legacy.get('profile_banner_url')
self.url: str = legacy.get('url')
self.location: str = legacy['location']
self.description: str = legacy['description']
self.description_urls: list = legacy['entities']['description']['urls']
self.urls: list = legacy['entities'].get('url', {}).get('urls')
self.pinned_tweet_ids: list[str] = legacy['pinned_tweet_ids_str']
self.is_blue_verified: bool = data['is_blue_verified']
self.verified: bool = legacy['verified']
self.possibly_sensitive: bool = legacy['possibly_sensitive']
self.can_dm: bool = legacy['can_dm']
self.can_media_tag: bool = legacy['can_media_tag']
self.want_retweets: bool = legacy['want_retweets']
self.default_profile: bool = legacy['default_profile']
self.default_profile_image: bool = legacy['default_profile_image']
self.has_custom_timelines: bool = legacy['has_custom_timelines']
self.followers_count: int = legacy['followers_count']
self.fast_followers_count: int = legacy['fast_followers_count']
self.normal_followers_count: int = legacy['normal_followers_count']
self.following_count: int = legacy['friends_count']
self.favourites_count: int = legacy['favourites_count']
self.listed_count: int = legacy['listed_count']
self.media_count = legacy['media_count']
self.statuses_count: int = legacy['statuses_count']
self.is_translator: bool = legacy['is_translator']
self.translator_type: str = legacy['translator_type']
self.withheld_in_countries: list[str] = legacy['withheld_in_countries']
self.location: str = legacy.get('location', '')
self.description: str = legacy.get('description', '')
self.description_urls: list = legacy.get('entities', {}).get('description', {}).get('urls', [])
self.urls: list = legacy.get('entities', {}).get('url', {}).get('urls')
Comment on lines +102 to +103

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Chained .get(..., {}) still breaks when intermediate values are explicitly None.

dict.get(key, default) only returns default when the key is missing; it returns None if the key is present with a None value. Looking at build_user_data in twikit/utils.py (used by follow_user/block_user/mute_user/etc.), legacy['entities'] is set via raw_data.get('entities'), so it can be None. In that case:

legacy.get('entities', {})          # -> None
    .get('description', {})         # -> AttributeError: 'NoneType' object has no attribute 'get'

This re-introduces the same class of crash the PR is trying to eliminate. Same concern applies to entities.description and entities.url being None.

Also, there is an inconsistency on Line 103: self.urls defaults to None while the docstring/type annotation says list, and self.description_urls on Line 102 defaults to []. Prefer [] for both to keep the attribute type stable for downstream consumers.

🛡️ Proposed fix
-        self.description_urls: list = legacy.get('entities', {}).get('description', {}).get('urls', [])
-        self.urls: list = legacy.get('entities', {}).get('url', {}).get('urls')
+        entities = legacy.get('entities') or {}
+        description_entities = entities.get('description') or {}
+        url_entities = entities.get('url') or {}
+        self.description_urls: list = description_entities.get('urls', [])
+        self.urls: list = url_entities.get('urls', [])

The same or {} pattern also covers the data.get('legacy', {}) on Line 91 being None if you want to harden that further:

-        legacy = data.get('legacy', {})
+        legacy = data.get('legacy') or {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.description_urls: list = legacy.get('entities', {}).get('description', {}).get('urls', [])
self.urls: list = legacy.get('entities', {}).get('url', {}).get('urls')
entities = legacy.get('entities') or {}
description_entities = entities.get('description') or {}
url_entities = entities.get('url') or {}
self.description_urls: list = description_entities.get('urls', [])
self.urls: list = url_entities.get('urls', [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@twikit/user.py` around lines 102 - 103, The code uses chained dict.get calls
on legacy which can be None (e.g., legacy.get('entities', {}) returns None if
entities exists but is None), causing AttributeError; change accesses in the
User initializer to defensively coalesce with or {} (e.g., use
(legacy.get('entities') or {}) then .get('description') or {} and .get('url') or
{}) and ensure both self.description_urls and self.urls default to empty lists
([]) not None to keep types stable; also consider hardening the earlier legacy
extraction in build_user_data by using (data.get('legacy') or {}) so legacy is
never None.

self.pinned_tweet_ids: list[str] = legacy.get('pinned_tweet_ids_str', [])
Comment on lines +102 to +104

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Align urls default with description_urls to consistently return a list.

description_urls always returns a list (defaulting to []), but urls may be None when entities/url is missing. This forces consumers to handle both None and list for similar fields. To keep the API consistent and predictable, consider defaulting urls to an empty list as well (e.g. ...get('urls', [])).

Suggested change
self.description_urls: list = legacy.get('entities', {}).get('description', {}).get('urls', [])
self.urls: list = legacy.get('entities', {}).get('url', {}).get('urls')
self.pinned_tweet_ids: list[str] = legacy.get('pinned_tweet_ids_str', [])
self.description_urls: list = legacy.get('entities', {}).get('description', {}).get('urls', [])
self.urls: list = legacy.get('entities', {}).get('url', {}).get('urls', [])
self.pinned_tweet_ids: list[str] = legacy.get('pinned_tweet_ids_str', [])

self.is_blue_verified: bool = data.get('is_blue_verified', False)
self.verified: bool = legacy.get('verified', False)
self.possibly_sensitive: bool = legacy.get('possibly_sensitive', False)
self.can_dm: bool = legacy.get('can_dm', False)
self.can_media_tag: bool = legacy.get('can_media_tag', False)
self.want_retweets: bool = legacy.get('want_retweets', False)
self.default_profile: bool = legacy.get('default_profile', False)
self.default_profile_image: bool = legacy.get('default_profile_image', False)
self.has_custom_timelines: bool = legacy.get('has_custom_timelines', False)
self.followers_count: int = legacy.get('followers_count', 0)
self.fast_followers_count: int = legacy.get('fast_followers_count', 0)
self.normal_followers_count: int = legacy.get('normal_followers_count', 0)
self.following_count: int = legacy.get('friends_count', 0)
self.favourites_count: int = legacy.get('favourites_count', 0)
self.listed_count: int = legacy.get('listed_count', 0)
self.media_count = legacy.get('media_count', 0)
self.statuses_count: int = legacy.get('statuses_count', 0)
self.is_translator: bool = legacy.get('is_translator', False)
self.translator_type: str = legacy.get('translator_type', '')
self.withheld_in_countries: list[str] = legacy.get('withheld_in_countries', [])
self.protected: bool = legacy.get('protected', False)

@property
Expand Down